| Tenouk C & C++ | MFC Home | COM Part 2 | COM Part 4 | Download | Site Index |


 

 

 

 

 

The Component Object Model - COM part 3

 

 

 

 

 

 

Program examples compiled using Visual C++ 6.0 compiler on Windows XP Pro machine with Service Pack 2. Topics and sub topics for this tutorial are listed below. Don’t forget to read Tenouk’s small disclaimer. The supplementary notes for this tutorial are OLE, mfc, COleObjectFactory class and .NET.

  1. Real COM with the MFC Library

  2. The COM CoGetClassObject() Function

  3. COM and the Windows Registry

  4. Runtime Object Registration

  5. How a COM Client Calls an In-Process Component

  6. How a COM Client Calls an Out-of-Process Component

  7. The MFC Interface Macros

  8. The MFC COleObjectFactory Class

  9. AppWizard/ClassWizard Support for COM In-Process Components

 

 

The Real COM with the MFC Library

 

So much for simulations. Now we'll get ready to convert the spaceship example to genuine COM. You need to acquire a little more knowledge before we start, though. First you must learn about the CoGetClassObject() function, then you must learn how COM uses the Windows Registry to load the component, and then you have to understand the difference between an in-process component (a DLL) and an out-of-process component (an EXE or a DLL running as a surrogate). Finally, you must become familiar with the MFC macros that support nested classes. The net result will be an MFC regular DLL component that contains all the CSpaceship code with the IMotion and IVisual interfaces. A regular MFC library Windows application acts as the client. It loads and runs the component when the user selects a menu item.

 

The COM CoGetClassObject() Function

 

In our simulation, we used a phony function named GetClassObject(). In real COM, we use the global CoGetClassObject() function. Co stands for "component object". Compare the following prototype to the GetClassObject() function you've seen already:

 

STDAPI CoGetClassObject(REFCLSID rclsid, DWORD dwClsContext,

    COSERVERINFO* pServerInfo, REFIID riid, LPVOID* ppvObj)

 

The interface pointer goes in the ppvObj parameter, and pServerInfo is a pointer to a machine on which the class object is instantiated (NULL if the machine is local). The types REFCLSID and REFIID are references to 128-bit GUIDs (globally unique identifiers for COM classes and interfaces). STDAPI indicates that the function returns a 32-bit value of type HRESULT. The standard GUIDs (for example, those GUIDs naming interfaces that Microsoft has already created) are defined in the Windows libraries that are dynamically linked to your program. GUIDs for custom classes and interfaces, such as those for spaceship objects, must be defined in your program in this way:

 

// {692D03A4-C689-11CE-B337-88EA36DE9E4E}

static const IID IID_IMotion = {0x692d03a4, 0xc689, 0x11ce, {0xb3, 0x37, 0x88, 0xea, 0x36, 0xde, 0x9e, 0x4e}};

 

If the dwClsContext parameter is CLSCTX_INPROC_SERVER, the COM subsystem looks for a DLL. If the parameter is CLSCTX_LOCAL_SERVER, COM looks for an EXE. The two codes can be combined to select either a DLL or an EXE, selected in order of performance. For example, inproc servers are fastest because everybody shares the same address space. Communication EXE servers are considerably slower because the interprocess calls involve data copying as well as many thread context switches. The return value is an HRESULT value, which is 0 (NOERROR) if no error occurs. Another COM function, CoCreateInstance(), combines the functionality of CoGetClassObject() and IClassFactory::CreateInstance.

 

 

COM and the Windows Registry

 

In the MYMFC28A example, the component was statically linked to the client, a clearly bogus circumstance. In real COM, the component is either a DLL or a separate EXE. When the client calls the CoGetClassObject() function, COM steps in and finds the correct component, which is located somewhere on disk. How does COM make the connection? It looks up the class's unique 128-bit class ID number in the Windows Registry. Thus, the class must be registered permanently on your computer.

If you run the Windows Regedit program (Regedt32 in Microsoft Windows NT 4/5), you'll see a screen similar to the one shown in Figure 11. This figure shows subfolders for four class IDs, three of which are class IDs associated with DLLs (InprocServer32) and one of which is a class ID associated with an EXE (LocalServer32). The CoGetClassObject() function looks up the class ID in the Registry and then loads the DLL or EXE as required. What if you don't want to track those ugly class ID numbers in your client program? No problem. COM supports another type of registration database entry that translates a human-readable program ID into the corresponding class ID. Figure 12 shows the Registry entries. The COM function CLSIDFromProgID() reads the database and performs the translation.

 

Figure 11:  Subfolders of four class IDs in the Registry.

 

Figure 11:  Subfolders of four class IDs in the Registry.

 

Figure 12:  Human-readable program IDs in the Registry.

 

Figure 12:  Human-readable program IDs in the Registry.

 

The first CLSIDFromProgID() parameter is a string that holds the program ID, but it's not an ordinary string. This is your first exposure to double-byte characters in COM. All string parameters of COM functions (except Data Access Objects (DAOs)) are Unicode character string pointers of type OLECHAR*. Your life is going to be made miserable because of the constant need to convert between double-byte strings and ordinary strings. If you need a double-byte literal string, prefix the string with an L character, like this:

 

CLSIDFromProgID(L"Spaceship", &clsid);

 

How does the registration information get into the Registry? You can program your component application to call Windows functions that directly update the Registry. The MFC library conveniently wraps these functions with the function COleObjectFactory::UpdateRegistryAll, which finds all your program's global class factory objects and registers their names and class IDs.

 

Runtime Object Registration

 

You've just seen how the Windows Registry registers COM classes on disk. Class factory objects also must be registered. It's unfortunate that the word "register" is used in both contexts. Objects in out-of-process component modules are registered at runtime with a call to the COM CoRegisterClassObject() function and the registration information is maintained in memory by the Windows DLLs. If the factory is registered in a mode that permits a single instance of the component module to create multiple COM objects, COM can use an existing process when a client calls CoGetClassObject().

 

How a COM Client Calls an In-Process Component

 

We're beginning with a DLL component instead of an EXE component because the program interactions are simpler. I'll show pseudocode here because you're going to be using the MFC library classes, which hide much of the detail.

 

Client

CLSID clsid;

IClassFactory* pClf;

IUnknown* pUnk;

CoInitialize(NULL);  // Initialize COM

CLSIDFromProgID("component_name", &clsid);

COM

 

COM uses the Registry to look up the class ID from "component_name".

 

Client

CoGetClassObject(clsid, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void**) &pClf);

COM

 

COM uses the class ID to look for a component in memory.

if (component DLL is not loaded already)

{

     COM gets DLL filename from the Registry

     COM loads the component DLL into process memory

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

DLL Component

if (component just loaded)

{

     Global factory objects are constructed

     DLL's InitInstance called (MFC only)

}

COM

 

COM calls DLL's global exported DllGetClassObject() with the CLSID value that was passed to CoGetClassObject().

 

DLL Component

 

DllGetClassObject() returns IClassFactory*.

 

COM

 

COM returns IClassFactory* to client.

 

Client

pClf->CreateInstance (NULL, IID_IUnknown, (void**) &pUnk);

DLL Component

 

Class factory's CreateInstance() function called (called directly, through component's vtable).

Constructs object of "componentname" class.

Returns requested interface pointer.

 

Client

pClf->Release();

pUnk->Release();

DLL Component

 

"component_name" Release() is called through vtable.

 

     if (refcount == 0)

     {

        Object destroys itself

     }

 

Client

CoFreeUnusedLibraries();

COM

 

COM calls DLL's global exported DllCanUnloadNow().

 

DLL Component

DllCanUnloadNow() called if (all DLL's objects destroyed)

{

     return TRUE

}

Client

CoUninitialize();  // COM frees the DLL if DllCanUnloadNow returns TRUE just prior to exit

COM

 

COM releases resources.

 

Client

 

Client exits.

 

DLL Component

 

Windows unloads the DLL if it is still loaded and no other programs are using it.

 

Some important points to note: first, the DLL's exported DllGetClassObject() function is called in response to the client's CoGetClassObject() call. Second, the class factory interface address returned is the actual physical address of the class factory vtable pointer in the DLL. Third, when the client calls CreateInstance(), or any other interface function, the call is direct (through the component's vtable). The COM linkage between a client EXE and a component DLL is quite efficient, as efficient as the linkage to any C++ virtual function in the same process, plus the full C++ parameter and return type-checking at compile time. The only penalty for using ordinary DLL linkage is the extra step of looking up the class ID in the Registry when the DLL is first loaded.

 

 

How a COM Client Calls an Out-of-Process Component

 

The COM linkage to a separate EXE component is more complicated than the linkage to a DLL component. The EXE component is in a different process, or possibly on a different computer. Don't worry, though. Write your programs as if a direct connection existed. COM takes care of the details through its remoting architecture, which usually involves Remote Procedure Calls (RPCs). In an RPC, the client makes calls to a special DLL called a proxy. The proxy sends a stream of data to a stub, which is inside a DLL in the component's process. When the client calls a component function, the proxy alerts the stub by sending a message to the component program, which is processed by a hidden window. The mechanism of converting parameters to and from data streams is called marshaling.

If you use standard interfaces (those interfaces defined by Microsoft) such as IClassFactory and IPersist (an interface we haven't seen yet but will appear when we examine COM persistence), the proxy and stub code, which implements marshaling, is provided by the Windows OLE32 DLL. If you invent your own interfaces, such as IMotion and IVisual, you need to write the proxies and stubs yourself. Fortunately, creating proxy and stub classes only involves defining your interfaces in Interface Definition Language (IDL) and compiling the code produced by the Microsoft Interface Definition Language (MIDL) compiler.

Here's the pseudocode interaction between an EXE client and an EXE component. Compare it to the DLL version found above. Notice that the client-side calls are exactly the same.

 

Client

CLSID clsid;

IClassFactory* pClf;

IUnknown* pUnk;

CoInitialize(NULL);  // Initialize COM

CLSIDFromProgID("component_name", &clsid);

COM

 

COM uses the Registry to look up the class ID from "component_name"

 

Client

CoGetClassObject(clsid, CLSCTX_LOCAL_SERVER, NULL, IID_IClassFactory, (void**) &pClf);

COM

 

COM uses the class ID to look for a component in memory.

 

     if (component EXE is not loaded already, or

     if we need another instance)

      {

           COM gets EXE filename from the Registry

           COM loads the component EXE

      }

 

EXE Component

if (just loaded)

{

     Global factory objects are constructed

     InitInstance called (MFC only)

     CoInitialize(NULL);

     for each factory object

     {

          CoRegisterClassObject(...);

          Returns IClassFactory* to COM

     }

     }

COM

 

COM returns the requested interface pointer to the client (client's pointer is not the same as the component's interface pointer).

 

Client

pClf->CreateInstance(NULL, IID_IUnknown, (void**) &pUnk);

EXE Component

 

Class factory's CreateInstance() function called (called indirectly through marshaling).

     Constructs object of "componentname" class.

     Returns requested interface pointer indirectly.

 

Client

pClf->Release();

pUnk->Release();

EXE Component

 

"component_name" Release() is called indirectly.

if (refcount == 0) {

     Object destroys itself

}

if (all objects released) {

     Component exits gracefully

}

Client

CoUninitialize();  // just prior to exit

COM

 

COM calls Release() for any objects this client has failed to release.

 

EXE Component

 

Component exits.

 

COM

 

COM releases resources.

 

Client

 

Client exits.

 

As you can see, COM plays an important role in the communication between the client and the component. COM keeps an in-memory list of class factories that are in active EXE components, but it does not keep track of individual COM objects such as the CSpaceship object. Individual COM objects are responsible for updating the reference count and for destroying themselves through the AddRef()/Release() mechanism. COM does step in when a client exits. If that client is using an out-of-process component, COM "listens in" on the communication and keeps track of the reference count on each object. COM disconnects from component objects when the client exits. Under certain circumstances, this causes those objects to be released. Don't depend on this behavior, however. Be sure that your client program releases all its interface pointers prior to exiting.

 

The MFC Interface Macros

 

In MYMFC28A, you saw nested classes used for interface implementation. The MFC library has a set of macros that automate this process. For the CSpaceship class, derived from the real MFC CCmdTarget class, you use the macros shown here inside the declaration.

 

BEGIN_INTERFACE_PART(Motion, IMotion)

    STDMETHOD_(void, Fly) ();

    STDMETHOD_(int&, GetPosition) ();

END_INTERFACE_PART(Motion)

 

BEGIN_INTERFACE_PART(Visual, IVisual)

    STDMETHOD_(void, Display) ();

END_INTERFACE_PART(Visual)

 

DECLARE_INTERFACE_MAP()

 

The INTERFACE_PART macros generate the nested classes, adding X to the first parameter to form the class name and adding m_x to form the embedded object name. The macros generate prototypes for the specified interface functions plus prototypes for QueryInterface(), AddRef(), and Release(). The DECLARE_INTERFACE_MAP macro generates the declarations for a table that holds the IDs of all the class's interfaces. The CCmdTarget::ExternalQueryInterface function uses the table to retrieve the interface pointers. In the CSpaceship implementation file, use the following macros:

 

BEGIN_INTERFACE_MAP(CSpaceship, CCmdTarget)

    INTERFACE_PART(CSpaceship, IID_IMotion, Motion)

    INTERFACE_PART(CSpaceship, IID_IVisual, Visual)

END_INTERFACE_MAP()

 

These macros build the interface table used by CCmdTarget::ExternalQueryInterface. A typical interface member function looks like this:

 

STDMETHODIMP_(void) CSpaceship::XMotion::Fly()

{

    METHOD_PROLOGUE(CSpaceship, Motion)

    pThis->m_nPosition += 10;

    return;

}

 

Don't forget that you must implement all the functions for each interface, including QueryInterface(), AddRef(), and Release(). Those three functions can delegate to functions in CCmdTarget. The STDMETHOD_ and STDMETHODIMP_ macros declare and implement functions with the __stdcall() parameter passing convention, as required by COM. These macros allow you to specify the return value as the first parameter. Two other macros, STDMETHOD and STDMETHODIMP, assume an HRESULT return value.

 

The MFC COleObjectFactory Class

 

In the simulated COM example, you saw a CSpaceshipFactory class that was hard-coded to generate CSpaceship objects. The MFC library applies its dynamic creation technology to the problem. Thus, a single class, aptly named COleObjectFactory, can create objects of any class specified at runtime. All you need to do is use macros like these in the class declaration:

 

DECLARE_DYNCREATE(CSpaceship)

DECLARE_OLECREATE(CSpaceship)

 

And use macros like these in the implementation file:

 

IMPLEMENT_DYNCREATE(CSpaceship, CCmdTarget)

// {692D03A3-C689-11CE-B337-88EA36DE9E4E}

IMPLEMENT_OLECREATE(CSpaceship, "Spaceship", 0x692d03a3, 0xc689, 0x11ce,

    0xb3, 0x37, 0x88, 0xea, 0x36, 0xde, 0x9e, 0x4e)

 

The DYNCREATE macros set up the standard dynamic creation mechanism. The OLECREATE macros declare and define a global object of class COleObjectFactory with the specified unique CLSID. In a DLL component, the exported DllGetClassObject() function finds the specified class factory object and returns a pointer to it based on global variables set by the OLECREATE macros. In an EXE component, initialization code calls the static COleObjectFactory::RegisterAll, which finds all factory objects and registers each one by calling CoRegisterClassObject(). The RegisterAll() function is called also when a DLL is initialized. In that case, it merely sets a flag in the factory object(s). We've really just scratched the surface of MFC's COM support. If you need more details, be sure to refer to Shepherd and Wingo's MFC Internals (Addison-Wesley, 1996) or MSDN online documentation.

 

AppWizard/ClassWizard Support for COM In-Process Components

 

AppWizard isn't optimized for creating COM DLL components, but you can fool it by requesting a regular DLL with Automation support. The following functions in the project's main source file are of interest:

 

BOOL CMymfc28BApp::InitInstance()

{

    COleObjectFactory::RegisterAll();

    return TRUE;

}

 

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)

{

    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    return AfxDllGetClassObject(rclsid, riid, ppv);

}

 

STDAPI DllCanUnloadNow(void)

{

    AFX_MANAGE_STATE(AfxGetStaticModule_State());

    return AfxDllCanUnloadNow();

}

 

STDAPI DllRegisterServer(void)

{

    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    COleObjectFactory::UpdateRegistryAll();

    return S_OK;

}

 

The three global functions are exported in the project's DEF file. By calling MFC functions, the global functions do everything you need in a COM in-process component. The DllRegisterServer() function can be called by a utility program such as regsvr32 to update the system Registry. Once you've created the skeleton project, your next step is to use ClassWizard to add one or more COM-creatable classes to the project. Just fill in the New Class dialog box, as shown here.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

-------------------------------------------------------------------------------

Figure 13: Adding new class to the COM project.

 

Figure 13: Adding new class to the COM project.

 

In your generated class, you end up with some Automation elements such as dispatch maps, but you can safely remove those. You can also remove (or commented out) the following two lines from StdAfx.h:

 

#include <afxodlgs.h>

#include <afxdisp.h>

 

COM and MFC - C++ code snippet

 

Listing 5.

 

 

Continue on next module......

 

 

 

 

 

 

Further reading and digging:

  1. DCOM at MSDN.

  2. COM+ at MSDN.

  3. COM at MSDN.

  4. Win32 process, thread and synchronization story can be found starting from Module R.

  5. MSDN MFC 7.0 class library online documentation.

  6. MSDN MFC 9.0 class library online documentation - latest version.

  7. MSDN Library

  8. Windows data type.

  9. Win32 programming Tutorial.

  10. The best of C/C++, MFC, Windows and other related books.

  11. Unicode and Multibyte character set: Story and program examples.

 

 

 

 

 


 

| Tenouk C & C++ | MFC Home | COM Part 2 | COM Part 4 | Download | Site Index |