Click Here to Install Silverlight*
United StatesChange|All Microsoft Sites
MSDN
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ
June 1995


Code for this article: activexcode0695.exe (6KB)
Don Box is a co-founder of DevelopMentor where he manages the COM curriculum. Don is currently breathing deep sighs of relief as his new book, Essential COM (Addison-Wesley), is finally complete. Don can be reached at http://www.develop.com/dbox/default.asp.

Q I've implemented several OLE automation servers using C++ and I'm able to access them using client programs written in Visual Basic®. Is it possible to access these and other automation-enabled applications from directly within my C++ programs?
Alan Waldcock
Portland, OR

A Writing automation clients in C++ is possible, although the run-time binding of function names and signatures that automation is based upon is diametrically opposed to the strongly typed nature of C++. Automation was initially designed for interpretive environments (such as Visual Basic) where type checking and enforcement occurs on the fly as statements are interpreted. In contrast, the design of C++ emphasizes compile-time type checking and enforcement for both efficiency and robustness. Since you are probably already writing in C++, the best approach to take when writing an automation client is to provide a static, strongly typed interface (or wrapper class) for the object you are accessing. Using a type-safe wrapper will allow you to write code using standard C++ programming techniques while still allowing you to interoperate with dynamically bound objects.
To create a C++ wrapper to an automation object, it helps to understand how Visual Basic automation clients work. Consider the following script:


 Sub CallHello ()
   Dim obj as Object
   Set obj = CreateObject("Hello.App")
   obj.Hello "SomeStringArg", 2.1
 End Sub
The script first creates a COM object, performs a QueryInterface to find its IDispatch interface, uses IDispatch::GetIDsOfNames to resolve the member name Hello to a DISPID, creates a DISPPARAMS struct to contain the two parameters, uses IDispatch::Invoke to invoke the Hello method, and finally calls IUnknown::Release on the outstanding interfaces to release and destroy the object. Who says Visual Basic is a toy language!
To translate the script above into C++ requires a fair amount of work (see Figure 1). Instantiating the object is relatively straightforward COM programming. The C++ version of CreateObject returns an IDispatch pointer that can be used to dynamically invoke member functions (unlike Visual Basic, C++ does not have a built-in type called Object). CreateObject needs to call CoCreateInstance to instantiate the object. CoCreateInstance requires a CLSID to find the server, so CreateObject must first translate the ProgID argument into a valid CLSID. There are two ways to name instantiable COM classes, CLSIDs and ProgIDs. A CLSID is the true name of the class, and is stored in the registry under HKEY_CLASSES_ROOT\ CLSID as a text-formatted GUID. A ProgID is an alternate human-readable classname and is stored directly under HKEY_CLASSES_ROOT. Both the CLSID and ProgID entries can maintain subkeys that identify the corresponding ProgID and CLSID, respectively.

 \Hello.App\CLSID={12345678-0123-...}
 \CLSID\{12345678-0123-...}\ProgID=Hello.App
COM provides two APIs to translate between the two name formats.

 HRESULT ProgIDFromCLSID(REFCLSID rclsid, 
                         LPOLESTR FAR* ppszProgId);
 HRESULT CLSIDFromProgID(LPCOLESTR pszProgID, 
                         LPCLSID pclsid);
To reduce the amount of context switching overhead, my version of CreateObject asks for the IDispatch interface as the initial interface, instead of first getting an initial IUnknown and then calling QueryInterface to request the IDispatch interface. If the object doesn't support IDispatch, there is no need to marshal an interface into your process, only to immediately release it.
IDispatch::Invoke, the member function that actually invokes the underlying method, requires a DISPID (currently typedefed to long int) to identify the method being invoked. To discover the DISPID at run time, the ResolveName helper function calls IDispatch::GetIDsOfNames to translate the text version of the name to its corresponding DISPID.

 HRESULT 
 IDispatch::GetIDsOfNames(REFIID riid,
                          LPOLECHAR FAR* rgszNames, 
                          unsigned int cNames,
                          LCID lcid,
                          DISPID FAR* rgdispid);
GetIDsOfNames takes an array of strings containing the member name followed by zero or more argument names, and if successful, fills in the supplied array of DISPIDs with the correct values. Since neither C++ nor Visual Basic 3.0 support named arguments, ResolveName only needs to pass the address of the member name string and DISPID and specify an array length of one (remember C pointers and arrays?). Automation allows an object to support multiple languages and countries simultaneously, so both GetIDsOfNames and Invoke take a locale ID (LCID) as a parameter to identify the language and national conventions the caller will be using. LCID zero is both language- and country- neutral, and is used in this example.
InvokeHello is a type-safe helper function that takes specifically typed parameters (in this case, a const string and a double) and invokes the "Hello" method with generic representations of the parameters using IDispatch::Invoke.

 HRESULT 
 IDispatch::Invoke(DISPID dispidMember,
                   REFIID riid,
                   LCID lcid,
                   unsigned short wFlags,
                   DISPPARAMS FAR* pdispparams,
                   VARIANT FAR* pvarResult,
                   EXCEPINFO FAR* pexcepinfo,
                   unsigned int FAR* puArgErr)
InvokeHello first attempts to resolve the name Hello to a DISPID and returns immediately if the lookup fails. IDispatch::Invoke requires that the method parameters be passed to it as an array of VARIANTARGs. VARIANTARGs are what is known in the RPC world as a discriminated union, that is, a structure that contains a type discriminator (called the tag) and a union of various data types (called the arm). Figure 2 contains a listing of the VARTYPEs (tags) and corresponding arm names and data types supported by VARIANTARGs.
In addition to the tag and arm, the VARIANTARG contains additional reserved members to properly align the arm on 8-byte boundaries (to support efficient copying of 8-byte doubles); these members (along with any un- used bytes in the arm) must be properly initialized with the VariantInit API. Once initialized, the tag and arm of each VARIANTARG are assigned the proper values. It is important to note that arguments are stored in the array in right to-left order. The DISPPARAMS structure bundles the VARIANTARG array with an array of DISPIDs that correspond to the names of each argument.

 typedef struct tagDISPPARAMS {
 // Array of arguments
     VARIANTARG FAR* rgvarg;
 // DISPIDS of named arguments
     DISPID FAR* rgdispidNamedArgs;
 // Number of arguments
     unsigned int cArgs; 
 // Number of named arguments
     unsigned int cNamedArgs; 
 } DISPPARAMS;
Since InvokeHello does not support named arguments, the DISPID-related members are set to zero.
You've probably encountered exceptions in a variety of contexts such as RPC exceptions, Win32
® structured exceptions, MFC exceptions, and ANSI C++ exceptions. Automation introduces its own form of exceptions that allows automation objects to throw exceptions in the client app. The exceptions that the object throws are not explicitly thrown in the C++ sense. Instead, the caller provides an EXCEPINFO structure for the object to fill in, and if an exception is thrown, the object will return DISP_E_EXCEPTION from IDispatch::Invoke. It is then up to the caller to decide how to catch the exception. InvokeHello simply displays a message box indicating the exception code that was thrown. The EXCEPINFO struct also can contain information about the source of the exception, a brief text description, and a help file and help context to provide additional information.
Once IDispatch::Invoke returns, any resources that were allocated in creating the VARIANTARG array must be released. Note that the string parameter (arg0) is not assigned to the VARIANTARG directly; instead, a system-maintained string is created that contains a copy of the string. Automation uses these system-maintained strings (known as BSTRs) whenever strings are passed to or from an object. BSTRs are length-prefixed, null-terminated strings that are allocated from a system-maintained heap, allowing efficient marshaling and safe reallocation when strings are passed by reference.
Given the code in Figure 1, it is fairly easy to construct a type-safe wrapper class that allows C++ clients to simply instantiate a C++ object and use it directly as if it were statically typed. Figure 3 contains an implementation of a minimal wrapper that allows construction and assignment from IDispatch pointers and uses the existing InvokeHello to implement the Hello member function. Given the implementation shown, you can now write code that looks much closer to the original Visual Basic script.

 void CallHello2(void)
 {
   CHelloApp obj;
   obj = CreateObject(OLESTR("Hello.App"));
   obj.Hello(OLESTR("SomeStringArg"), 2.1);
 }
The normal approach would have been to write the wrapper class first, implementing the helper functions that are now global as member functions. For a small class like HelloApp (which has only one member), creating the wrapper is trivial. For a more complex class with dozens of member functions, it would be nice to get some help. Enter MFC.
MFC provides a class called COleDispatchDriver that implements most of the code needed to access automation objects. COleDispatchDriver is meant to be used as a base class for type-safe wrappers, but could conceivably be used directly if needed. As shown in Figure 4, COleDispatchDriver has the standard suite of wrapper functions (AttachDispatch, DetachDispatch, ReleaseDispatch) and a set of Dispatch-related convenience functions (InvokeHelper, SetProperty, GetProperty). InvokeHelper is the most interesting member, and the primary reason this class is as useful as it is.
COleDispatchDriver::InvokeHelper allows you to call the underlying IDispatch::Invoke using printf-style format strings to describe a va_arg-based argument list (note the ellipsis as the last argument).

 void 
 COleDispatchDriver::InvokeHelper(DISPID dwDispID,
                                  WORD wFlags,
                                  VARTYPE vtRet,
                                  void* pvRet, 
                                  const BYTE* pbParamInfo,
                                   ...);
Note that there isn't a VARIANTARG to be found anywhere in the parameter list. Instead, the caller specifies the types of the parameters in the "string" pointed to by pbParamInfo. MFC defines a family of VTS string constants that contain the ASCII value of the VARTYPE they correspond to. For example, the system header files contain the following definitions:

 // from OLEAUTO.h
 enum VARENUM { ..., VT_I2 = 2, ... };
 // from AFXDISP.h
 #define VTS_I2 "\x02"
To create a paramInfo string, you can simply concatenate the desired VTS symbols into a single string. Both C and C++ allow a single string literal to be formed from multiple string literals delimited by whitespace, so paramInfo strings are easily produced and used as follows:

 const BYTE paramInfo[] = VTS_BSTR VTS_R8;
 InvokeHelper(dispid, DISPATCH_METHOD, VT_EMPTY, 0,
              paramInfo, szArg0, dblArg1);
The paramInfo array contains the ASCII values 8 (VT_BSTR), 5 (VT_R8), and 0 (VT_NULL). The implementation of InvokeHelper uses this array to traverse the remaining call stack, forming proper VARIANTARGs along the way.
Figure 5 shows an automation client class based on COleDispatchDriver. The intermediate class, COleDynamicDriver, is introduced to allow DISPIDs to be translated at run time. Note that the implementation of CHelloApp2:: Hello caches the DISPID returned from ResolveName in a static local variable to avoid multiple lookups of the same member name. Given the high cost of context switching encountered when using out-of-proc servers, spending eight bytes of static storage per method is a reasonable tradeoff.
Figure 6 Generating an automation wrapper with ClassWizard
Figure 6 Generating an automation wrapper with ClassWizard

Ideally, you'd like to resolve the DISPID of all members during the development process and hardcode them into the wrapper functions, avoiding run-time resolution altogether. After all, you did hardcode the type signature of the function statically, so it seems reasonable that you could also statically bind the DISPID. Well, maybe.
Automation servers come with varying levels of support for helping out developers of external clients. It turns out that there are two commonly known forms of supporting documentation, and a third "secret" form known only to those whose recreational computing time is spent using SDK tools instead of playing Doom. The most common form of support is a WinHelp file documenting the automation interface(s) supported by an application. These files are usually written for programmers working in Visual Basic, but are helpful to anyone wanting to understand the semantics of the exposed objects. The second, somewhat less common form of support is type libraries. Modern automation-enabled applications should come with a TLB file that contains a binary description of the data types exposed by the application. Given a TLB file, it is trivial to use ClassWizard to generate a type-safe wrapper to an automation server. Figure 6 shows ClassWizard in action, and Figure 7 shows the generated code (which was automatically added to the current project, thank you very much). In an ideal world, all automation servers would provide both a WinHelp and a TypeLib file. Unless Microsoft
® Excel is the only automation server you intend on using, your world is probably less than ideal, which means you have a bit of detective work ahead of you.
Figure 8 Retrieving TypeInfo for WordBasic
Figure 8 Retrieving TypeInfo for WordBasic

What about that third form of documentation? I came across it quite by accident one day while playing around with OLE2View from the OLE 2 SDK. OLE2View allows you to browse registered type libraries and instantiate objects to list their supported interfaces. Some of the interfaces that appear in the list have inspectors that are run when you double-click the interface name. For example, double-clicking IDataObject displays the data formats that the object supports by calling IDataObject:: EnumFormatEtc. Guess what happens when you double-click IDispatch? Right. OLE2View calls IDispatch::GetTypeInfo to get a description of the interface from the object. OLE2View then provides a reasonable browser of the TypeInfo associated with the object. From this, you can deduce what operations an object supports as well as the DISPIDs and type signatures of each member. Based on this information, you can manually generate a type-safe wrapper class using COleDispatchDriver. Figures 8 and 9 show OLE2View displaying the TypeInfo for Word Basic (unfortunately, Microsoft Word does not provide a TypeLib file), and Figure 10 shows the type-safe wrapper based on a subset of Word's exposed interface. Given the type-safe wrapper from Figure 10, you can now start to use Microsoft Word to implement large chunks of functionality in your application. For example, the following code puts the date on the Clipboard as both text, a metafile, and a Word embedding.

 void PutDateOnClipboard(void) {
   WordBasic MSWord;
   if (MSWord.CreateDispatch("Word.Basic"))   {
       MSWord.FileNewDefault();
       MSWord.InsertDateTime(OLESTR("d MMMM,yyyy"), 
                            FALSE);
       MSWord.EditSelectAll();
       for (int i = 0; i < 15; i++)
         MSWord.GrowFont();
       MSWord.EditCopy();
   }
 }
Figure 9 Displaying TypeInfo for WordBasic
Figure 9 Displaying TypeInfo for WordBasic

Before concluding, I should note that there is a major compatibility problem with the current implementation of COleDispatchDriver and some automation servers (including Microsoft Word version 6). If a method does not return a value (that is, its return type is void), the OLE documentation states that the caller should pass a NULL pointer for the VARIANT * used to store the method result. COleDispatchDriver:: InvokeHelper does not do this. Instead, it passes a valid pointer to a VARIANT whose VARTYPE field is set to VT_EMPTY. Unfortunately, some automation servers explicitly verify that this pointer is NULL when invoking a method with no return value, and raise a Dispatch exception if it is not. This makes COleDispatchDriver virtually useless for interoperating with these servers. Fortunately, you can repair the problem yourself by deriving a new class (I call mine COleDispatchDriverRepaired) and reimplementing InvokeHelper and InvokeHelperV using the powerful editor inheritance feature built into the Visual C++ TextWizard (that means, copy the code from OLEDISP2.CPP and paste it into your new file). The defect is (fortunately) isolated to exactly one line in InvokeHelperV. In the call to IDispatch::Invoke, change the statement from

 // make the call
 SCODE sc = 
 m_lpDispatch->Invoke(dwDispID, IID_NULL, 0,
                      wFlags, &dispparams,
                      &vaResult, 
                      &excepInfo, &nArgErr);
to

 // make the call
 SCODE sc = 
 m_lpDispatch->Invoke(dwDispID, IID_NULL, 0,
                      wFlags, &dispparams,
               (vtRet != VT_EMPTY ? &vaResult : 0), 
                      &excepInfo, &nArgErr);
This minor change solves most of the automation compatibility problems while you wait for the next CD to come from on high. (This bug has been repaired in MFC 3.1.) Be aware that since none of the members of COleDispatchDriver are virtual, this solution is less than elegant C++ code. It is, however, superior to rebuilding (and reversioning) the MFC DLL just to change one defect.

Have a question about programming with ActiveX or COM? Send your questions via email to Don Box at dbox@develop.com or http://www.develop.com/dbox/default.asp

From the June 1995 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe .

© 1997 Microsoft Corporation. All rights reserved.
Terms of Use
.

© 2017 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft