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 subsytem 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 EX24A 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 3.51), you'll see a screen similar to the one shown in Figure 24-5. 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 24-6 shows the Registry entries. The COM function CLSIDFromProgID reads the database and performs the translation.

Click to view at full size.

Figure 24-5. Subfolders of four class IDs in the Registry.

Click to view at full size.

Figure 24-6. 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);

You'll begin learning about the MFC library's Unicode string conversion capabilities in Chapter 25.

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("componentname", &clsid);

COM

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

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

"componentname" 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("componentname", &clsid);

COM

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

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

"componentname" 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 EX24A, 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 as described in Appendix B. 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).

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 CEx24bApp::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 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.

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 the following two lines from StdAfx.h:

#include <afxodlgs.h>
#include <afxdisp.h>

MFC COM Client Programs

Writing an MFC COM client program is a no-brainer. You just use AppWizard to generate a normal application. Add the following line in StdAfx.h:

#include <afxole.h>

Then add the following line at the beginning of the application class InitInstance member function:

AfxOleInit();

You're now ready to add code that calls CoGetClassObject.

The EX24B Example—An MFC COM In-Process Component

The EX24B example is an MFC regular DLL that incorporates a true COM version of the CSpaceship class you saw in EX24A. AppWizard generated the ex24b.cpp and ex24b.h files, as described previously. Figure 24-7 shows the Interface.h file, which declares the IMotion and IVisual interfaces. Figures 24-8 and 24-9 show the code for the CSpaceship class. Compare the code to the code in EX24A. Do you see how the use of the MFC macros reduces code size? Note that the MFC CCmdTarget class takes care of the reference counting and QueryInterface logic.

INTERFACE.H

struct IMotion : public IUnknown
{
    STDMETHOD_(void, Fly) () = 0;
    STDMETHOD_(int&, GetPosition) () = 0;
};

struct IVisual : public IUnknown
{
    STDMETHOD_(void, Display) () = 0;
};

Figure 24-7. The Interface.h file. ,

SPACESHIP.H

void ITrace(REFIID iid, const char* str);

//////////////////////////////////////////////////////////////////////
// CSpaceship command target

class CSpaceship : public CCmdTarget
{
    DECLARE_DYNCREATE(CSpaceship)

private:
    int m_nPosition; // We can access this from all the interfaces
    int m_nAcceleration;
    int m_nColor;
protected:
    CSpaceship();    // protected constructor used by dynamic creation

// Attributes
public:

// Operations
public:

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CSpaceship)
    public:
    virtual void OnFinalRelease();
    //}}AFX_VIRTUAL

// Implementation
protected:    
    virtual ~CSpaceship();

    // Generated message map functions
    //{{AFX_MSG(CSpaceship)
        // NOTE - the ClassWizard will add and remove member 
        //  functions here.
    //}}AFX_MSG

    DECLARE_MESSAGE_MAP()
    DECLARE_OLECREATE(CSpaceship)
    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()
};
//////////////////////////////////////////////////////////////////////

Figure 24-8. The Spaceship.h file.

SPACESHIP.CPP

#include "stdAfx.h"
#include "ex24b.h"
#include "Interface.h"
#include "Spaceship.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE
__;
#endif
//////////////////////////////////////////////////////////////////////
// CSpaceship

// {692D03A4-C689-11CE-B337-88EA36DE9E4E}
static const IID IID_IMotion =
{ 0x692d03a4, 0xc689, 0x11ce,
    { 0xb3, 0x37, 0x88, 0xea, 0x36, 0xde, 0x9e, 0x4e } };

// {692D03A5-C689-11CE-B337-88EA36DE9E4E}
static const IID IID_IVisual =
{ 0x692d03a5, 0xc689, 0x11ce,
    { 0xb3, 0x37, 0x88, 0xea, 0x36, 0xde, 0x9e, 0x4e } };

IMPLEMENT_DYNCREATE(CSpaceship, CCmdTarget)
CSpaceship::CSpaceship()
{
    TRACE("CSpaceship ctor\n");
    m_nPosition = 100;
    m_nAcceleration = 101;
    m_nColor = 102;
    // To keep the application running as long as an OLE automation
    //  object is active, the constructor calls AfxOleLockApp.

    AfxOleLockApp();
}

CSpaceship::~CSpaceship()
{
    TRACE("CSpaceship dtor\n");
    // To terminate the application when all objects created with
    //  OLE automation, the destructor calls AfxOleUnlockApp.
    
    AfxOleUnlockApp();
}
void CSpaceship::OnFinalRelease()
{
    // When the last reference for an automation object is released
    //  OnFinalRelease is called. This implementation deletes the 
    //  object. Add additional cleanup required for your object before
    //  deleting it from memory.

    delete this;
}
BEGIN_MESSAGE_MAP(CSpaceship, CCmdTarget)
    //{{AFX_MSG_MAP(CSpaceship)
    // NOTE - ClassWizard will add and remove mapping macros here.
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

BEGIN_INTERFACE_MAP(CSpaceship, CCmdTarget)
    INTERFACE_PART(CSpaceship, IID_IMotion, Motion)
    INTERFACE_PART(CSpaceship, IID_IVisual, Visual)
END_INTERFACE_MAP()

// {692D03A3-C689-11CE-B337-88EA36DE9E4E}
IMPLEMENT_OLECREATE(CSpaceship, "Spaceship", 0x692d03a3, 0xc689,
                    0x11ce, 0xb3, 0x37, 0x88, 0xea, 0x36, 0xde,
                    0x9e, 0x4e)
STDMETHODIMP_(ULONG) CSpaceship::XMotion::AddRef()
{
    TRACE("CSpaceship::XMotion::AddRef\n");
    METHOD_PROLOGUE(CSpaceship, Motion)
    return pThis->ExternalAddRef();
}

STDMETHODIMP_(ULONG) CSpaceship::XMotion::Release()
{
    TRACE("CSpaceship::XMotion::Release\n");
    METHOD_PROLOGUE(CSpaceship, Motion)
    return pThis->ExternalRelease();
}

STDMETHODIMP CSpaceship::XMotion::QueryInterface(
    REFIID iid, LPVOID* ppvObj)
{
    ITrace(iid, "CSpaceship::XMotion::QueryInterface");
    METHOD_PROLOGUE(CSpaceship, Motion)
    return pThis->ExternalQueryInterface(&iid, ppvObj);
}
STDMETHODIMP_(void) CSpaceship::XMotion::Fly()
{
    TRACE("CSpaceship::XMotion::Fly\n");
    METHOD_PROLOGUE(CSpaceship, Motion)
    TRACE("m_nPosition = %d\n", pThis->m_nPosition);
    TRACE("m_nAcceleration = %d\n", pThis->m_nAcceleration);
    return;
}

STDMETHODIMP_(int&) CSpaceship::XMotion::GetPosition()
{
    TRACE("CSpaceship::XMotion::GetPosition\n");
    METHOD_PROLOGUE(CSpaceship, Motion)
    TRACE("m_nPosition = %d\n", pThis->m_nPosition);
    TRACE("m_nAcceleration = %d\n", pThis->m_nAcceleration);
    return pThis->m_nPosition;
}

//////////////////////////////////////////////////////////////////////
STDMETHODIMP_(ULONG) CSpaceship::XVisual::AddRef()
{
    TRACE("CSpaceship::XVisual::AddRef\n");
    METHOD_PROLOGUE(CSpaceship, Visual)
    return pThis->ExternalAddRef();
}

STDMETHODIMP_(ULONG) CSpaceship::XVisual::Release()
{
    TRACE("CSpaceship::XVisual::Release\n");
    METHOD_PROLOGUE(CSpaceship, Visual)
    return pThis->ExternalRelease();
}

STDMETHODIMP CSpaceship::XVisual::QueryInterface(
    REFIID iid, LPVOID* ppvObj)
{
    ITrace(iid, "CSpaceship::XVisual::QueryInterface");

    METHOD_PROLOGUE(CSpaceship, Visual)
    return pThis->ExternalQueryInterface(&iid, ppvObj);
}
STDMETHODIMP_(void) CSpaceship::XVisual::Display()
{
    TRACE("CSpaceship::XVisual::Display\n");
    METHOD_PROLOGUE(CSpaceship, Visual)
    TRACE("m_nPosition = %d\n", pThis->m_nPosition);
    TRACE("m_nColor = %d\n", pThis->m_nColor);
}

//////////////////////////////////////////////////////////////////////
void ITrace(REFIID iid, const char* str)
{
    OLECHAR* lpszIID;
    ::StringFromIID(iid, &lpszIID);
    CString strTemp = (LPCWSTR) lpszIID;
    TRACE("%s - %s\n", (const char*) strTemp, (const char*) str);
    AfxFreeTaskMem(lpszIID);
}

//////////////////////////////////////////////////////////////////////
// CSpaceship message handlers

Figure 24-9. The Spaceship.cpp file.

The EX24C Example—An MFC COM Client

The EX24C example is an MFC program that incorporates a true COM version of the client code you saw in EX24A. This is a generic AppWizard MFC Single Document Interface (SDI) EXE program with an added #include statement for the MFC COM headers and a call to AfxOleInit, which initializes the DLL. A Spaceship option on an added Test menu is mapped to the view class handler function shown in Figure 24-10. The project also contains a copy of the EX24B component's Interface.h file, shown in Figure 24-7. You can see an #include statement for this file at the top of ex24cView.cpp.

void CEx24cView::OnTestSpaceship() 
{
    CLSID clsid;
    LPCLASSFACTORY pClf; 
    LPUNKNOWN pUnk;
    IMotion* pMot;
    IVisual* pVis;

    HRESULT hr;
    if ((hr = ::CLSIDFromProgID(L"Spaceship", &clsid)) != NOERROR) {
        TRACE("unable to find Program ID -- error = %x\n", hr);
        return;
    }
    if ((hr = ::CoGetClassObject(clsid, CLSCTX_INPROC_SERVER,
        NULL, IID_IClassFactory, (void **) &pClf)) != NOERROR) {;
        TRACE("unable to find CLSID -- error = %x\n", hr);
        return;
    }

    pClf->CreateInstance(NULL, IID_IUnknown, (void**) &pUnk);
    pUnk->QueryInterface(IID_IMotion, (void**) &pMot); // All three
    pMot->QueryInterface(IID_IVisual, (void**) &pVis); //  pointers
                                                       //  should work
    TRACE("main: pUnk = %p, pMot = %p, pDis = %p\n", pUnk, pMot, pVis);

    // Test all the interface virtual functions
    pMot->Fly();
    int nPos = pMot->GetPosition();
    TRACE("nPos = %d\n", nPos);
    pVis->Display();

    pClf->Release();
    pUnk->Release();
    pMot->Release();
    pVis->Release();
    AfxMessageBox("Test succeeded. See Debug window for output.");
}

Figure 24-10. The client's command handler that loads and tests the CSpaceship component.

To test the client and the component, you must first run the component to update the Registry. Several utilities can be used to do this, but you might want to try the REGCOMP program in the \vcpp32\RegComp project on the companion CD-ROM. This program prompts you to select a DLL or an OCX file, and then it calls the exported DllRegisterServer function.

Both client and component show their progress through TRACE calls, so you need the debugger. You can run either the client or the component from the debugger. If you try to run the component, you'll be prompted for the client pathname. In either case, you don't have to copy the DLL because Windows finds it through the Registry.