July 1998
George Shepherd is a Senior Software Engineer at Stingray Software where he develops Visual CASE. Scot Wingo is cofounder of Stingray Software (www.stingsoft.com), an MFC/Java class library company. George and Scot co-wrote MFC Internals (Addison-Wesley, 1996).
|
Q When I create a new class in Visual C++® using the ClassWizard, the easiest way to put a COM wrapper around it is to check the Automation option. But how do I create the COM wrapper for existing classes? I realize that I have to add a lot of stuff manually, but what I am looking for is a set of steps to follow for accomplishing this.
Keshav Deshpande
A If you've spent some time working with Visual C++ and MFC's version of Automation, you know what a cool thing Automation can be. It's sometimes tempting to get lulled into the simplicity of the implementation of Automation in Visual C++. It's really easy to implement IDispatch using MFC and the ClassWizard, especially if you're starting fresh. Just click the right checkbox and you're off and running. But what if you've got some legacy MFC code you want to wrap with COM Automation?
Clicking on the Automation checkbox adds a lot of stuff to your source and header filesstuff that you have to type in by hand if you forget to mark the checkbox or if you have some legacy code that you want to COMify. Here's a step-by-step list of exactly what you need to do to add Automation to your MFC project's source and header files. Step 1 If it's not there already, add AFXDISP.H to your project's STDAFX.H file. This adds all the COM-oriented definitions necessary to make Automation work in your project. AFXDISP.H brings OBJBASE.H, OLEAUTO.H, OLECTL.H, and OCIDL.H into your project. Step 2 If you want to use the MFC implementation of IDispatch, make sure the class to which you want to add Automation is derived from CCmdTarget. Like the MFC messaging routing system, the MFC implementation of IDispatch depends upon some virtual functions being overridden in your CCmdTarget-derived C++ class. Step 3 Next, you need to beef up your class's constructor a bit. Add a call to EnableAutomation to your class's constructor. Here's how EnableAutomation works. The MFC implementation of IDispatch lives inside a class named COleDispatchImpl, which resides within a file named OLEDISP1.CPP. CCmdTarget has a member variable named m_xDispatch that holds a pointer to an implementation of IDispatch. EnableAutomation takes COleDispatchImpl's vtable pointer (which points to the MFC implementation of IDispatch) and places that address inside CCmdTarget's m_xDispatch member variable. CCmdTarget's first entry into its interface map is always IID_IDispatch, and the value corresponding to that entry is NULL by default. In effect, calling EnableAutomation fills the first interface map position with a pointer to COleDispatchImpl's IDispatch vptr. In addition to calling EnableAutomation in your constructor, you should also add a call to AfxOleLockApp in your class's constructor. COM servers maintain reference counts on the Mselves so they can determine how many extant objects they are serving. AfxOleLockApp manages that counter. Finally, your class's destructor needs to call AfxOleUnlockApp to balance out the server's lock count when the Object is finally released. This code shows how to call EnableAutomation and AfxOleLockApp in your CCmdTarget class's constructor and AfxOleUnlockApp in the destructor:
Step 4 In the MFC implementation of IDispatch, a lookup table maps dispatch IDs to your C++ class's member functions. To get Automation working requires that you add a dispatch map to your class. That's really just a matter of adding a couple of macros to your class's header and source files. The DECLARE_DISPATCH_MAP macro goes in the header file, while the BEGIN_DISPATCH_MAP and the END_DISPATCH_MAP macros go in the source file. The ClassWizard also expects to see certain comments in two places so it knows where to place the code added through ClassWizard. The first set of comments tells ClassWizard where to place member variable and functions exposed through IDispatch. the Se comments are located within the header file. The second set of comments appears between the dispatch map entries in the source code file. It tells ClassWizard where to put the dispatch map entries. Be sure to include it. Figure 1 shows how the dispatch map macros and comments should appear in your source code. Be sure to use the exact spacing as shown and include the name of the class to which you're adding Automation wherever called for.
Step 5 Add an interface map and an entry for your specific version of IDispatch. IDispatch is already part of CCmdTarget's makeup, and calling EnableAutomation turns on CCmdTarget's IDispatch support. This means that clients like VBScript can successfully QueryInterface your object for IDispatch (it's automatically in the interface map). Sometimes clients will QueryInterface for your object's IDispatch pointer, but they will know it by a different name. While there is only one signature for IDispatch, each object uses it to expose a different set of properties and methods. That is, each particular instance of IDispatch can be named by a different GUID. Clients will know the GUID to use to ask for your interface because the GUID will be advertised in the type librarymore on that later. If you use ClassWizard or AppWizard to add Automation to your class automatically, the wizards generate some code that names your particular instance of IDispatch and handles QueryInterface interface requests for it. If you want this facility to be available from your object, you'll want to name your version of IDispatch and add an interface map to handle QueryInterface requests for the interface. Figure 2 shows how to add code to handle QueryInterface requests for your interface. You'll need to use GUIDGEN (a small app that comes with Visual C++) to create a GUID and put it in your source code file. As with dispatch maps, adding interface maps involves macrosspecifically DECLARE_INTERFACE_MAP, BEGIN_INTERFACE_MAP, and END_INTERFACE_MAP. You don't need to be wary of any comments in this case because there's no ClassWizard support for adding this facility to your class. Step 6 Add code to turn on COM. This step is necessary if you're placing your COM class in an EXE server. COM has a requirement that before a thread makes any COM- specific API calls, the thread must call CoInitialize. If you're planting your COM class inside an EXE server, your InitInstance code should call AfxOleInit (which thunks down to calling CoInitialize). |
|
AfxOleInit is preferable to calling CoInitialize because AfxOleInit causes the application to call CoUninitialize when shutting down. (This is not necessary if you're putting your class inside a DLL.)
Step 7 At this point, your C++ class has basic IDispatch support. If the class is living inside an EXE, the EXE has called CoInitialize, thereby enabling COM to work. To allow your C++ class to play in the COM game, your C++ class needs a class object (what folks sometime call a class factory). The MFC class object support is available through the COleObjectFactory class. The nice thing about MFC is that you won't have to touch the COleObjectFactory yourselfyou can put the class object support into your class by adding a couple of macrosDECLARE_OLECREATE and IMPLEMENT_OLECREATEto your C++ class. the Se macros add a static member of type COleObjectFactory to your class. When clients call CoCreateInstance to create instances of your class, COleObjectFactory uses the MFC dynamic creation mechanism to create the new class (this means that your class must also use the DECLARE_DYNCREATE and the IMPLEMENT_DYNCREATE macros as well). You'll need to get a GUID for the COM class (just use GUIDGEN again). The first argument for IMPLEMENT_OLECREATE is the class to which you're adding Automation; the second parameter for IMPLEMENT_OLECREATE is a ProgID; and the third argument is the GUID for the class. Figure 3 shows the code for adding class object support for your class. There's an alternative version of this step if you're trying to automate the application's document. Remember that the MFC document/view architecture requires three classes (the document, the frame, and the view) to be used whenever a document is opened or created. the Se three classes are tied together using MFC's document template. If the document is automated, then the class object has to know about all three classes (not just a single class). MFC has a class named COleTemplateServer (derived from COleObjectFactory) that acts as the class object for an MFC document supporting Automation. When clients call CoCreateInstance on a COM class that happens to use the MFC document/view architecture, all three components must be created. To accomplish this, the application should have a variable of type COleTemplateServer. COleTemplateServer knows how to create the document/frame/view components when creating a document. Here's how to declare the COleTemplateServer inside the application: |
|
As with the DECLARE_OLECREATE and IMPLEMENT_OLECREATE macros, the document-based class must also use the MFC dynamic creation mechanism, so the document must also have the DECLARE_DYNAMIC and IMPLEMENT_DYNAMIC macros.
The document implementing Automation must connect the class object to the document template. Figure 4 shows how to declare a GUID to represent the class ID (notice how it's used during the call to ConnectTemplate). Step 8 Add registration and registry updating code. After you've added class object support to your COM class, MFC requires that you register the class objects with MFC before they can be used. In addition to registering the class objects at runtime, well-behaved servers add their own registry entries so that clients don't have to. If your class lives in a DLL, the CWinApp-derived object in your DLL will also have an InitInstance call. If you don't have a CWinApp-derived class in your DLL source code, you can just add one and create a global instance of it. To make the class object support work in a DLL, call COleObjectFactory::RegisterAll. |
|
Note that RegisterAll is a static function, and that you
don't need an instance of COleObjectFactory to call RegisterAll. This function rips through the project's class objects and calls CoRegisterClassObject for each one. While RegisterAll doesn't do much to the class objects in a DLL (it just sets a flag), you still need to call it because MFC won't use the class objects until you call RegisterAll. RegisterAll does a bit more if your classes live in an EXE (as you'll see in a moment).
A DLL updates its registry entries when called upon through the DLL's DllRegisterServer function. The class object has a function named UpdateRegistryAll that inserts the correct information in the registry. Notice that UpdateRegistryAll is also a static function of COleObjectFactory. If you're putting your COM class inside an EXE, then the Object registration should happen during InitInstance as well. Call the class object's RegisterAll function if the server is being run as an Automation server or an embedding server (you can get this information from the command line). Be sure to call COleObjectFactory::UpdateRegistryAll inside InitInstance as well. Figure 5 shows how to call RegisterAll and UpdateRegistryAll. Step 9 Add a type library to your project. There's one last step to take before using ClassWizard to add Automation properties and methods. One of the most important components of a COM DLL is its type library, which provides binary information about your COM class so that lots of different environments can use it. For example, if you'd like to attract developers using Visual Basic to your program, you'll want to give the M a type library to make their lives easier. Then they can just read in the type information to your class and begin using it right away. Type libraries are generated through Object Definition Language (ODL). If you checked the Automation button in the AppWizard when you first created the project, you'll already have some skeleton ODL code to work with. If you didn't check the Automation button, then you'll need to create the ODL file from scratch and add it to your project. You can use the Project | Add To Project | Files option to add the ODL to your project. Figure 6 shows the ODL file. ODL syntax is always of the format "attributes followed by a thing." For example, notice in Figure 6 that the library keyword is preceded by a pair of square braces with some attributes (the GUID and the version of the library) between the M. The importlib statement is like including windows.h in your applicationit contains a bunch of predefined things you'll need in your ODL file. The second entry in the ODL file is the description of the interface you're adding to your class (denoted by the keyword dispinterface). Notice that the dispinterface keyword is also preceded by some attributes lying between two square braces. the Only attribute you really need for the dispinterface is a GUID. For client tools like Visual Basic to work properly, make sure this is the same GUID as you used in the INTERFACE_PART macro earlier. The properties and the methods keywords indicate the interface's properties and methods. Notice the comments following the properties and the methods keywords. ClassWizard expects to see the Se comments there because as you use ClassWizard to embellish your Automation interface, it will keep the ODL file up to date. The final entry in the ODL file advertises the class that is implementing the dispatch interface. The coclass keyword denotes a COM class that implements certain interfaces. Make sure the GUID used to identify this COM class is the same GUID used in the IMPLEMENT_OLECREATE macro or the COleTemplateServer::ConnectServer function. In this case, the coclass statement simply tells clients they should be able to successfully QueryInterface for IDispatch and IAClassWithAutomation when using the AClassWithAutomation class. Step 10 Next comes the easy part. Just go to the ClassWizard. If you've added the macros correctly for the dispatch maps, then ClassWizard will understand how to manipulate the code inside the dispatch maps. Go ahead and add properties and methods to your heart's content. While it's trivial to add Automation to your projectas long as you select it from the beginningthere are times that you'd like to add Automation to a legacy project. It's not really hard to accomplish this; it just requires a bit of typing. Have a question about programming in Visual Basic, Visual FoxPro, Microsoft Access, Office, or stuff like that? Send your questions via email to George Shepherd at 70023.1000@compuserve.com. |
From the July 1998 issue of Microsoft Systems Journal.