Sometimes our COM component must send a notification to the client or call the callback function. The scheme is simple: the component publishes the interface, the client creates an object inherited from the interface and passes it to the component, the component in turn calls the interface functions, thereby calling the functions on the client side.
In the case of Visual Basic or Visual Basic for Applicatons, we can write a class inherited from any interface, but this is not possible for VBScript script files.
Here the IDispatch
interface rushes to our aid. Using this interface, our powerful component will humbly assume the modest role of a client, and a small script will turn into a real automation server.
We will develop the component in the FreeBASIC programming language.
You can declare and use classes in script files. Such classes are implicitly inherited from the IDispatch
interface and are real COM classes.
We declare a class, an instance of which we will subsequently pass to our component:
Class CallBack Function CallBack(Param) ' WScript.Echo Param CallBack = 0 End Function End Class
Our component will receive an instance of the CallBack
class, call the CallBack
function and pass it a string with text in the parameter.
' Dim Component Set Component = CreateObject("BatchedFiles.TestCOMServer") ' , Dim objCallBack Set objCallBack = New CallBack ' Component.SetCallBack objCallBack, "" ' result = Component.InvokeCallBack() WScript.Echo result Set objCallBack = Nothing Set Component = Nothing
This interface is a stumbling block to automation. Typically, the implementation of IDispatch
based on a type library through ITypeInfo->Invoke
or the CreateStdDispatch
function, but in this case the automation server is located in a script and does not have a type library, and our component acts as a client. To simplify, IDipatch
works like this: takes the name of the function and transfers control to it.
The definition of the interface lies in the header โoaidl.biโ (indents and line breaks are added for readability):
Type IDispatch As IDispatch_ Type LPDISPATCH As IDispatch Ptr Type IDispatchVtbl ' IUnknown Dim InheritedTable As IUnknownVtbl GetTypeInfoCount As Function( _ ByVal this As IDispatch Ptr, _ ByVal pctinfo As UINT Ptr _ )As HRESULT GetTypeInfo As Function( _ ByVal this As IDispatch Ptr, _ ByVal iTInfo As UINT, _ ByVal lcid As LCID, _ ByVal ppTInfo As ITypeInfo Ptr Ptr _ )As HRESULT GetIDsOfNames As Function( _ ByVal this As IDispatch Ptr, _ ByVal riid As Const IID Const Ptr, _ ByVal rgszNames As LPOLESTR Ptr, _ ByVal cNames As UINT, _ ByVal lcid As LCID, _ ByVal rgDispId As DISPID Ptr _ )As HRESULT Invoke As Function( _ ByVal this As IDispatch Ptr, _ ByVal dispIdMember As DISPID, _ ByVal riid As Const IID Const Ptr, _ ByVal lcid As LCID, _ ByVal wFlags As WORD, _ ByVal pDispParams As DISPPARAMS Ptr, _ ByVal pVarResult As VARIANT Ptr, _ ByVal pExcepInfo As EXCEPINFO Ptr, _ ByVal puArgErr As UINT Ptr _ )As HRESULT End Type Type IDispatch_ lpVtbl As IDispatchVtbl Ptr End Type
The GetIDsOfNames
and Invoke
functions are most interesting in this interface.
It takes the name of the function and returns its dispatch identifier DISPID
. DISPID
is an alias for the LONG
type.
From the clientโs point of view, DISPID
is simply an optimization tool that avoids passing strings. For the server, DISPID
is the identifier of the function that the client wants to call.
Parameter | Description |
---|---|
riid | Reserved. A pointer to IID_NULL should be passed. |
rgszNames | An array of function names for which it is necessary to return dispatch identifiers. |
cNames | The size of the array. |
lcid | Localization information. |
rgDispId | An array where the function will write DISPID
for each function name or DISPID_UNKNOWN
if it does not find a function with that name. |
By dispatch identifier performs the corresponding function.
Parameter | Description |
---|---|
dispIdMember | Dispatcher identifier of the called function. |
riid | Reserved. A pointer to IID_NULL should be passed. |
lcid | Localization information. |
wflags | Flags type functions. For simple functions, set to DISPATCH_METHOD
, to get the property value - DISPATCH_PROPERTYGET
, to set the property value - DISPATCH_PROPERTYPUT
, by reference - DISPATCH_PROPERTYPUTREF
. |
pDispParams | Special structure with function call parameters. |
pVarResult | Pointer to the type VARIANT
where the function will bring the result of the work. |
pExcepInfo | A pointer to the structure where the function will write the thrown exception. Can be set to NULL
. |
puArgErr | The indices of the arguments that caused the error. Can be set to NULL
. |
This structure contains the parameters of the called function. All parameters are packaged in VARIANT
.
Type tagDISPPARAMS ' rgvarg As VARIANTARG Ptr ' rgdispidNamedArgs As DISPID Ptr ' cArgs As UINT ' cNamedArgs As UINT End Type Type DISPPARAMS As tagDISPPARAMS
To simplify the code, we will not use named arguments, we will set NULL
instead.
For use in scripts, components should also directly or indirectly inherit from IDipatch
.
ITestCOMServer
build the ITestCOMServer
interface with two functions SetCallBack
and InvokeCallBack
. The first will save the automation server object, the second will call the object function.
Type ITestCOMServer As ITestCOMServer_ Type LPITESTCOMSERVER As ITestCOMServer Ptr Type ITestCOMServerVirtualTable ' IDispatch Dim InheritedTable As IDispatchVtbl Dim SetCallBack As Function( _ ByVal this As ITestCOMServer Ptr, _ ByVal CallBack As IDispatch Ptr, _ ByVal UserName As BSTR _ )As HRESULT Dim InvokeCallBack As Function( _ ByVal this As ITestCOMServer Ptr _ )As HRESULT End Type Type ITestCOMServer_ Dim pVirtualTable As ITestCOMServerVirtualTable Ptr End Type
Now you can declare a COM โ class:
Type TestCOMServer ' Dim pVirtualTable As ITestCOMServerVirtualTable Ptr ' Dim ReferenceCounter As ULONG ' Dim CallBackObject As IDispatch Ptr ' Dim UserName As BSTR End Type
The implementation of the SetCallBack
function SetCallBack
simple: we save the automation server object transmitted by the client and the function call parameter.
Function TestCOMServerSetCallBack( _ ByVal pTestCOMServer As TestCOMServer Ptr, _ ByVal CallBack As IDispatch Ptr, _ ByVal UserName As BSTR _ )As HRESULT ' , If pTestCOMServer->CallBackObject <> NULL Then IDispatch_Release(pTestCOMServer->CallBack) End If pTestCOMServer->CallBackObject = CallBack ' If pTestCOMServer->CallBackObject <> NULL Then IDispatch_AddRef(pTestCOMServer->CallBack) End If ' SysFreeString(pTestCOMServer->UserName) ' pTestCOMServer->UserName = SysAllocStringLen(UserName, SysStringLen(UserName)) Return S_OK End Function
But the InvokeCallBack
function will InvokeCallBack
to work hard. First you need to get the dispatcher identifier of the CallBack
function of the automation server.
Function TestCOMServerInvokeCallBack( _ ByVal pTestCOMServer As TestCOMServer Ptr _ )As HRESULT If pTestCOMServer->CallBack = NULL Then Return E_POINTER End If ' Const cNames As UINT = 1 ' Dim rgszNames(cNames - 1) As WString Ptr = {@"CallBack"} ' DISPID Dim rgDispId(cNames - 1) As DISPID = Any Dim hr As HRESULT = IDispatch_GetIDsOfNames( _ pTestCOMServer->CallBackObject, _ @IID_NULL, _ @rgszNames(0), _ cNames, _ GetUserDefaultLCID(), _ @rgDispId(0) _ ) If FAILED(hr) Then MessageBoxW(NULL, " DISPID", NULL, MB_OK) Return E_FAIL End If
After the DISPID
function is received, it can be called:
' ยซ, %UserName%ยป Dim Greetings As BSTR = SysAllocString(", ") Dim GreetingsUserName As BSTR = Any VarBstrCat(Greetings, pTestCOMServer->UserName, @GreetingsUserName) Const ParamsCount As Integer = 1 ' Dim varParam(ParamsCount - 1) As VARIANT = Any For i As Integer = 0 To ParamsCount - 1 VariantInit(@varParam(i)) Next ' โ varParam(0).vt = VT_BSTR varParam(0).bstrVal = GreetingsUserName Dim Params(0) As DISPPARAMS = Any Params(0).rgvarg = @varParam(0) Params(0).cArgs = ParamsCount Params(0).rgdispidNamedArgs = NULL Params(0).cNamedArgs = 0 ' Dim VarResult As VARIANT = Any Dim ExcepInfo As EXCEPINFO = Any Dim uArgErr As UINT = Any ' hr = IDispatch_Invoke( _ pTestCOMServer->CallBackObject, _ rgDispId(0), _ @IID_NULL, _ GetUserDefaultLCID(), _ DISPATCH_METHOD, _ @Params(0), _ @VarResult, _ NULL, _ NULL _ ) ' For i As Integer = 0 To ParamsCount - 1 VariantClear(@varParam(i)) Next SysFreeString(Greetings) Return S_OK End Function
As you can see, even with a script file, a component can receive feedback. This is useful for notifying the client of completed operations by the component.
Classes in scripts can be registered in the registry, in which case they will be available for the whole system using ProgID
, but this is a completely different story.
Project code on the github site: https://github.com/zamabuvaraeu/TestCOMServer
PS Somehow the highlight for the BASIC syntax disappeared, instead it used VBScript, and some operators are not highlighted with it.