IDispatch has some problems that have made it somewhat unpopular with COM+ developers. The most serious of these problems is the performance of IDispatch-based components. Components using IDispatch can be accessed in two slightly different ways, known as early binding and late binding. Originally, IDispatch was designed so that every call to IDispatch::Invoke to invoke a method was preceded by a call to IDispatch::GetIDsOfNames. The client called GetIDsOfNames to get the DISPID of a particular method in the component. For example, if a client wanted to call the Sum method of an object, it would first call GetIDsOfNames to learn the DISPID of that method. This DISPID would then be passed as the first parameter to IDispatch::Invoke to actually call the Sum method. Subsequent calls to the Sum method could still call GetIDsOfNames to get the DISPID, or the client could cache the DISPID so that later calls to Sum could simply be made via a single call to Invoke.
This original way of using IDispatch is known as late binding. The main advantage of this technique is that a type library isn't required. The problem with late binding, however, is that two round-trips to the component are required for at least the first call to each method. With early binding, the component's type library is consulted to obtain the necessary DISPIDs, which are then hard-coded into the application so that at run time the client has to make only a single call to IDispatch::Invoke, omitting the call to IDispatch::GetIDsOfNames. This optimization is based on information from a type library and theoretically doubles the execution speed of each Automation-based method call compared with the late binding technique. However, not even early binding comes close to the performance of accessing an interface directly via its v-table, as is the case with custom interfaces.
Using the IDispatch interface from languages such as Visual Basic and VBScript couldn't be easier. These languages automatically make all the necessary calls to the four IDispatch methods. However, due to the flexibility and power of the IDispatch interface, creating Automation clients in C++ is more difficult than calling methods in a custom interface—another reason why Microsoft recommends that COM+ objects support dual interfaces. The IDispatch interface makes the object accessible to scripting languages such as VBScript; a custom interface makes the object easier for C++ developers to use.
Automation clients written in C++ follow a rather predictable series of steps. After initializing COM+ (CoInitializeEx) and instantiating a class (CoCreateInstance), the client calls QueryInterface for the IDispatch interface. If this is successful, the client has a pointer to the IDispatch interface and knows that it is dealing with an Automation object. Now we arrive at the fundamental step of calling the IDispatch methods, as shown in the following code. First IDispatch::GetIDsOfNames should be called to retrieve the DISPID of the desired method. Since our example component supports only a single Sum method with a known DISPID of 1, we could skip this step and use early binding. For demonstration purposes, however, we'll show the call to GetIDsOfNames. Note that since all COM+ interfaces use Unicode strings, the string shown in this code fragment is prefixed with the letter L, indicating to the compiler that these are wide characters of the primitive type wchar_t.9
OLECHAR* name = L"Sum"; DISPID dispid; pDispatch->GetIDsOfNames(IID_NULL, &name, 1, GetUserDefaultLCID(), &dispid); |
IDispatch::GetIDsOfNames is called using the name of the Sum method and returns the DISPID of that method as the last parameter. In this example, the dispid variable should return the value 1, which was defined as the DISPID in the IDL file. Although our IDispatch implementation ignores the locale information passed by GetIDsOfNames, it is still polite to provide the user's default LCID, which is retrieved by calling GetUserDefaultLCID. If in the future the component is upgraded to support localization, the information provided by the client will be correct.
After retrieving the DISPID, the client calls the specified method using IDispatch::Invoke. The hardest part about using the Invoke method is packing the DISPPARAMS structure with the required method parameters. The DISPPARAMS structure is shown below in IDL notation:
typedef struct tagDISPPARAMS { // Array of arguments [size_is(cArgs)] VARIANTARG* rgvarg; // Array of DISPIDs of named arguments [size_is(cNamedArgs)] DISPID* rgdispidNamedArgs; // Total number of arguments UINT cArgs; // Number of named arguments UINT cNamedArgs; } DISPPARAMS; |
The first member of the DISPPARAMS structure is the actual pointer to an array of method arguments. It is declared as a VARIANTARG10 pointer. Each VARIANTARG contains a vt member that indicates the data type. This member should be set to the appropriate VT_* constant for the type of data stored in the VARIANTARG.
The second and fourth parameters are used for named arguments, and the third parameter specifies the total number of parameters. Thus, in the simplest case—a call to an Automation method with no parameters—the DISPPARAMS structure should be filled out as follows:
DISPPARAMS NoParams = { NULL, NULL, 0, 0 }; |
For methods such as Sum that expect to receive parameters, the arguments in the array should be arranged from last to first so that rgvarg[0] contains the last argument and rgvarg[cArgs _ 1] contains the first argument. The following code packs two 4-byte integers into an array of two variants for the purposes of adding 2 and 7. The VariantInit function is called to initialize a new VARIANTARG to VT_EMPTY. Note that while the order of the Sum method's two parameters doesn't really matter (2 + 7 = 7 + 2), the order of the parameters in the example is 2, 7. This difference would of course be crucial in a method with different semantics.
VARIANTARG SumArgs[2]; VariantInit(&SumArgs[0]); SumArgs[0].vt = VT_I4; SumArgs[0].lVal = 7; VariantInit(&SumArgs[1]); SumArgs[1].vt = VT_I4; SumArgs[1].lVal = 2; |
Once you set the VARIANTARG's parameters, you must then create a DISPPARAMS structure that points to the array of parameters. As shown in Figure 5-6, the DISPPARAMS structure is initialized with a pointer to SumArgs, and the cArgs counter is set to 2, indicating that there are two arguments in the array. Since no named parameters are used, the second and fourth parameters are ignored.
Figure 5-6. The DISPPARAMS structure created for invoking the Sum method with the parameters 2 and 7.
Before IDispatch::Invoke is called, one final variant must be initialized in which the return value of the Sum method will be stored. After all this work, Invoke is finally called, as shown here:
VARIANT Result; VariantInit(&Result); HRESULT hr = pDispatch->Invoke(dispid, IID_NULL, GetUserDefaultLCID(), DISPATCH_METHOD, &MyParams, &Result, NULL, NULL); if(FAILED(hr)) cout << "pDispatch->Invoke() failed. " << endl; cout << "2 + 7 = " << Result.lVal << endl; pDispatch->Release(); |
You can use named parameters in addition to positional arguments when you invoke Automation-based methods. Actually, the term named parameters is a bit of a misnomer since the parameters are actually identified based on their DISPID value. Parameters are automatically assigned a DISPID value based on their position within the argument list. For example, the Sum method has three parameters that are implicitly assigned DISPIDs as follows:
HRESULT Sum([optional, defaultvalue(-1)] int x, // DISPID = 0 [optional, defaultvalue(-1)] int y, // DISPID = 1 [out, retval] int* retvalue); // DISPID = 2 |
Therefore the Sum method can be invoked with the DISPPARAMS structure filled out as shown in Figure 5-7. This causes the Sum method to be called with the parameters 7, 2; the Visual Basic equivalent is Sum(y:=7, x:=2). The order of the named parameters in the array is not important, although they are generally passed in reverse order.
Figure 5-7. The DISPPARAMS structure created for invoking the Sum method with two named parameters.
To omit an optional parameter when you use named arguments, you just don't pass it. For example, the declaration below calls the Sum method with only the y parameter:
VARIANTARG SumArg; VariantInit(&SumArg); SumArg.vt = VT_I4; SumArg.lVal = 5; DISPID DispId = 1; // The y parameter DISPPARAMS Params = { &SumArg, &DispId, 1, 1 }; // Now IDispatch::Invoke calls ISum::Sum(-1, 5) |
When you use position arguments, you must still define optional parameters. However, you should set the type to VT_ERROR and the value to DISP_E_PARAMNOTFOUND, indicating that the parameter is not actually being passed, as shown below:
VARIANTARG SumArgsOpt[2]; VariantInit(&SumArgsOpt[0]); SumArgsOpt[0].vt = VT_ERROR; SumArgsOpt[0].scode = DISP_E_PARAMNOTFOUND; // normally y VariantInit(&SumArgsOpt[1]); SumArgsOpt[1].vt = VT_I4; SumArgsOpt[1].lVal = 3; // normally x DISPPARAMS Params = { SumArgsOpt, NULL, 2, 0 }; // Now IDispatch::Invoke calls ISum::Sum(3, -1) |
Working with Automation in C++ can be a trying experience, but in Visual Basic it's a breeze. In the following code, Visual Basic's CreateObject function instantiates the component based on a programmatic identifier. In the registry, under HKEY_CLASSES_ROOT, you'll find a ton of partially legible entries known as program identifiers (ProgIDs), organized alphabetically. Like a CLSID, a ProgID identifies a class, but with less precision. Since ProgIDs are not guaranteed to be unique, you can use them only where name collisions are manageable—for example, on a single machine. Every ProgID key should have a CLSID subkey containing the CLSID of the object. Given a ProgID, an application can use the CLSIDFromProgID function to retrieve the CLSID.
Most ProgIDs come in pairs. The version-independent ProgID is followed by the standard ProgID. The format of a version-independent ProgID is Component.Class; the two elements are separated by a period with no spaces, as in Word.Document. The format of the standard ProgID is identical, but with a version number at the end, as in Word.Document.8. The version-independent ProgID remains constant across all versions of a class. It is often used with high-level languages such as Visual Basic and always refers to the latest installed version of the application's class.
The RegisterServer function called in the component's self-registration code creates the ProgID registry entries, as shown in Figure 5-8.
Figure 5-8. The ProgID registry entries, shown in the Registry Editor.
Without further delay, here is the entire Visual Basic program that calls the Automation component created earlier in this chapter:
Private Sub Form_Click() Dim myRef As Object Set myRef = CreateObject("Component.InsideCOM") Print "Sum(4, 6) = " & myRef.Sum(4, 6) End Sub |
Because no design-time reference is set to a type library, the CreateObject method uses IDispatch via the late binding technique, meaning that IDispatch::GetIDsOfNames is called before IDispatch::Invoke. If you want to use the early binding technique, you must set a reference to the component's type library by choosing References from the Project menu in Visual Basic and selecting the component. Visual Basic will obtain the DISPIDs for the interface method names at design time by using the information stored in the type library and thus needs to call only IDispatch::Invoke at run time.
Some people mistakenly assume that any object instantiated using the CreateObject function is always accessed via the IDispatch interface. Although CreateObject returns an IDispatch pointer, this pointer does not necessarily mean that you are stuck with that interface. The culprit is Visual Basic's Object type, which is the equivalent of an IDispatch pointer. Whenever a variable is declared As Object, the IDispatch interface is used. To overcome this limitation, you simply use the IDispatch pointer returned by CreateObject and call IUnknown::QueryInterface to request some other interface pointer. As we saw in Chapter 3, a call to QueryInterface from Visual Basic is performed using the Set statement to cast one type to another. For example, the code shown below does not use the IDispatch interface at all, even though the CreateObject function is called. This is possible because Visual Basic reads the information stored in the component's type library.
Dim myRef As InsideCOM Set myRef = CreateObject("Component.InsideCOM") Print "Sum(4, 6) = " & myRef.Sum(4, 6) |
The Set statement in the preceding code fragment calls QueryInterface on the IDispatch pointer returned by CreateObject to request a pointer to the ISum interface. When the Sum method is invoked, Visual Basic calls ISum::Sum—not IDispatch::Invoke. Since you can use CreateObject to instantiate an object that is not accessed through IDispatch, you might wonder what the difference is between the CreateObject function and the New keyword. The primary difference is that the New keyword uses the CLSID obtained from the type library, while CreateObject obtains the CLSID from the registry by calling the CLSIDFromProgID function. The lesser known difference is that New uses an internal creation mechanism when it instantiates a class in this part of the active application, while CreateObject always uses the COM+ activation method.11 The following code fragment uses the New keyword to access an object via IDispatch:
Dim myDispatch As Object Dim myRef As New InsideCOM ' myRef->QueryInterface(IID_IDispatch, (void**)&myDispatch); Set myDispatch = myRef Print "Sum(4, 6) = " & myDispatch.Sum(4, 6) |
If you are curious about how Visual Basic's CreateObject function is implemented, here is C++ pseudocode for the call.
IDispatch* CreateObject(LPCOLESTR szProgID) { CLSID clsid; IDispatch* pDispatch = 0; CLSIDFromProgID(szProgID, &clsid); CoCreateInstance(clsid, 0, CLSCTX_SERVER, IID_IDispatch, (void**)&pDispatch); return pDispatch; } |