sean's happy programming place

The Journals of Captain COM:
Simple Servers

Sean Baxter

 

Download the source and executables for this article.  In this entry of The Journals Of Captain COM, we'll implement the two servers introduced in part I.

A DLL Server

A COM server is, essentially, a coclass's binary packaging.   All full-featured COM servers do three things: register their coclasses (see part I), control their lifetimes, and expose their class factories.  As you probably know, COM servers (in general) come in two forms: DLLs (aka OCXs) and EXEs.  DLL servers are in-process: the objects they provide live in the address space of the client.  DLL servers have a significant speed advtantage over their EXE counterparts: the objects they expose may run in the caller's apartment, eliminating the need for thread switches at every method call.  The interface methods of in-process server objects can also accept non-remotable types including GDI handles.  Speed and low memory overhead make the DLL server the best choice for most components.

EXE servers are launched by the COM runtime in response to CoGetClassObject (the same function that maps DLL servers).  Because they run in their own process, EXE server components won't kill the client when they crash.  This no-hose guarantee makes EXE servers the choice for applications where stability is central.  All clients must access EXE server components through IPC proxy/stub objects (with the rare exception of clients sharing the same EXE with their components), so making method calls on local components is a very slow operation.  The EXE server does provide a few useful features, however.  EXE servers can be launched by remote clients with the NT Service Control Manager (NT SCM) and their components accessed (with the help of the DCOM runtimes) as easy as pie.  Also, by exposing a singleton class factory, an EXE server can facilitate communication between any number of processes (local or remote).

So we begin with the DLL server implementation.  In addition to DllRegisterServer and DllUnregisterServer, our COM server exports the function DllCanUnloadNow.  This function is called periodically by the COM API CoFreeUnusedLibraries, which in response to DllCanUnloadNow's return value may unmap and unload the DLL with CoFreeLibrary.  An S_FALSE return value keeps the server alive; S_OK returns it to nature.  The most common way of implementing this export function involves a static counter of outstanding objects of the server.  Objects increment this counter in their constructor and decrement it in their destructor.  Only when the counter is zero will the server let itself die.

The DLL server exposes its class factories through the export function DllGetClassObjectCoGetClassObject consults the registry to find the location of the component's server.  If an InprocServer32 key exists, the COM runtime loads the specified DLL and invokes its exported DllGetClassObject function.  The server creates an instance of the requested class factory (identified by CLSID) and returns an interface pointer to it.

Let's resume the dlldemo1.cpp file from where we left off in part I:

from dlldemo1.cpp

// stuff above here is from the previous article

long moduleCount;

extern "C" HRESULT _stdcall DllCanUnloadNow() {
    return moduleCount ? S_FALSE : S_OK;
}

class CDlldemo1 : public IDlldemo1 {
    ULONG _ref;
    CComBSTR _bstrMessage;
public:
    CDlldemo1() : _ref(0) {
        InterlockedIncrement(&moduleCount);
        _bstrMessage = L"This is the default message";
    }
    ~CDlldemo1() { InterlockedDecrement(&moduleCount); }

    // from IUnknown
    virtual ULONG
_stdcall AddRef() { return ++_ref; }
   
virtual ULONG _stdcall Release() {
        ULONG ret(--_ref);
        if(!ret) delete this;
       
return ret;
    }
   
virtual HRESULT _stdcall QueryInterface(REFIID riid, void** ppv) {
       
if(!ppv) return E_POINTER;
       
if(riid == IID_IUnknown) *ppv = this;
        else
if(riid == IID_IDlldemo1) *ppv = this;
       
else return *ppv = 0, E_NOINTERFACE;
       
return AddRef(), S_OK;
    }

    // from IDlldemo1
   
virtual HRESULT _stdcall HelloMessage() {
        char* ansiMessage = new char[_bstrMessage.Length() + 1];
        WideCharToMultiByte(CP_ACP, 0, _bstrMessage.m_str,
            _bstrMessage.Length() + 1, ansiMessage,
            _bstrMessage.Length() + 1, 0, 0);
        MessageBox(0, ansiMessage, "HelloMessage() called",
            MB_OK | MB_ICONEXCLAMATION);
       
delete ansiMessage;
       
return S_OK;
    }
   
virtual HRESULT _stdcall HelloMessage2(BSTR bstrHi) {
        char* ansiMessage = new char[SysStringLen(bstrHi) + 1];
        WideCharToMultiByte(CP_ACP, 0, bstrHi, SysStringLen(bstrHi) + 1,
            ansiMessage, SysStringLen(bstrHi) + 1, 0, 0);
        MessageBox(0, ansiMessage, "HelloMessage2() called",
            MB_OK | MB_ICONEXCLAMATION);
       
delete ansiMessage;
       
return S_OK;
    }
   
virtual HRESULT _stdcall get_Message(BSTR* pBstrHi) {
       
if(!pBstrHi) return E_POINTER;
        _bstrMessage.CopyTo(pBstrHi);
       
return S_OK;
    }
   
virtual HRESULT _stdcall put_Message(BSTR bstrHi) {
        _bstrMessage = bstrHi;
       
return S_OK;
    }
};

The global variable moduleCount lives at the top of this source chunk.  It is, quite clearly, the counter of outstanding objects in the server.  DllCanUnloadNow returns S_FALSE if moduleCount is non-zero, telling the COM runtime to refrain from unloading the server.  Look at the CDlldemo1 constructor and destructor.  They increment/decrement the counter to keep it up-to-date.  Notice that the thread-safe functions InterlockedIncrement and InterlockedDecrement are used here.  We specified in the ATL Registrar script that Dlldemo1 can live in any single-threaded apartment.   The thread-safe calls are needed to ensure atomicity of the increment and decrement operations; without them, two components could muck with moduleCount at the same time, leading to disastrous results.  If the component had thread model Single, concurrent access of the counter would not be possible, and the C++ increment and decrement operators (++ and --) would be appropriate. 

The CDlldemo1 class has only two data members: the BSTR (Basic String) of the default message, _bstrMessage (for the methods of our custom interface IDlldemo1), and the object's reference counter, _ref.  With few exceptions, all COM objects are required to manage their own lifetimes.  The CDlldemo1 constructor sets _ref to zero.  Why not 1?  Because the class factory will immediately follow with a call to QueryInterface, which increments the counter, before returning an interface pointer to the caller.  The method AddRef increments the reference counter, and Release decrements the counter.  When the counter is decremented to zero, the object deletes itself, and its destructor decrements the object counter moduleCount.  Notice that AddRef and Release use the thread-unsafe operators ++ and --.  Thread safety is not an issue (in general) with components that live in an STA.  All calls to their methods are serialized through the thread's message pump, so concurrent access to any of the object's data members is impossible.  If the dlldemo1 component had lived in a multi-threaded apartment instead (marked with ThreadingModel = Free in the registry), the reference counter would have to be incremented and decremented with the thread-safe functions InterlockedIncrement and InterlockedDecrement.

CDlldemo1's QueryInterface constructor is as simple as it gets.  If the out parameter ppv is zero, QueryInterface returns the standard error code E_POINTER.   Otherwise, it checks the IID parameter against each COM interface the class implements.  CDlldemo1 implements two interfaces: IDlldemo1 and its base class IUnknown.  If the IID refers to either of these interfaces, the out parameter ppv is set to the address of the interface's virtual table pointer.  Because IDlldemo1 (and it's base interface IUnknown) is the CDlldemo1's first inherited interface (it's the only inherited interface), its virtual table pointer comprises the first four bytes of the object.  Therefore, we can assign to ppv the this pointer.  If the client had QueryInterfaced for an interface that wasn't the first item in the inheritance list, a static_cast operation to add the virtual table pointer's offset to this would be necessary.  static_casts inside QueryInterface are also often necessary to disambiguate between interfaces that are inherited multiple times.   More sophisticated QueryInterface approaches will be covered in later articles.

The implementations of the IDlldemo1 interface methods show how truly easy it is to deliver functionality through COM objects.  The methods HelloMessage, HelloMessage2, and the property Message are implemented as regular C++ virtual functions, yet they exhibit great benefits of COM: language transparency and location transparency.  Our HelloMessage implementation simply displays a message box with the text stored in CDlldemo1::_bstrMessage.   This text is initially set to "This is the default message" by the class's constructor.  Because BSTRs are length-prefixed UNICODE strings (the ATL class CComBSTR encapsulates a BSTR and provides a few nifty functions), we need to convert _bstrMessage to an ANSI character string with the Win32 API function WideCharToMultiByte.

HelloMessage2 displays the in parameter bstrHi in a message box.  The COM runtime function SysStringLen extracts the length-prefix on the specified BSTR to calculate the length of the string.  This is significantly faster than a function like strlen, which loops through a string until it finds the null terminator.

The Message property maps to the C++ funtions get_Message and put_Message (this is unique to MIDL-generated headers; there is no requirement that these prefixes be used in all C++ implementations).  The Visual C++ extension __declspec(property) can be used to make working with COM properties more convienent (check the VC++ docs).  In the get_Message implementation, CComBSTR::CopyTo is called to deep-copy the default string to the out parameter.  Likewise, the assignment in put_Message deep-copies the in parameter string to the private data member _bstrMessage.  

CDlldemo1 is a fully-functional COM class.   However, there is yet no way for the client to instantiate instances of the class, as non-in process or non-C++ clients can't directly invoke operator new on the class.  The COM solution to this dilemma is the class factory.  Class factories are themselves complete COM objects with the purpose of instantiating other COM classes.  Here is the class factory source for the dlldemo1 component:

from dlldemo1.cpp
       
class CDlldemo1Factory : public IClassFactory {
    ULONG _ref;
public:
    CDlldemo1Factory() : _ref(0) {
        InterlockedIncrement(&moduleCount);
    }
    ~CDlldemo1Factory() { InterlockedDecrement(&moduleCount); }
   
    // from IUnknown
   
virtual ULONG _stdcall AddRef() { return ++_ref; }
   
virtual ULONG _stdcall Release() {
        ULONG ret(--_ref);
       
if(!ret) delete this;
       
return ret;
    }
   
virtual HRESULT _stdcall QueryInterface(REFIID riid, void** ppv) {
       
if(!ppv) return E_POINTER;
       
if(riid == IID_IUnknown) *ppv = this;
        else
if(riid == IID_IClassFactory) *ppv = this;
       
else return *ppv = 0, E_NOINTERFACE;
       
return AddRef(), S_OK;
    }

    // from IClassFactory
   
virtual HRESULT _stdcall CreateInstance(IUnknown* pOuter, REFIID riid,
       
void** ppv) {
       
if(!ppv) return E_POINTER;
       
if(pOuter) return *ppv = 0, CLASS_E_NOAGGREGATION;
        CDlldemo1* pDlldemo1 = new CDlldemo1;
        HRESULT hr = pDlldemo1->QueryInterface(riid, ppv);
       
if(hr) delete pDlldemo1;
       
return hr;
    }
   
virtual HRESULT _stdcall LockServer(BOOL fLock) { return S_OK; }
};

extern "C" HRESULT
_stdcall DllGetClassObject(REFCLSID rclsid,
                                              REFIID riid, void** ppv) {
   
if(!ppv) return E_POINTER;
   
if(rclsid != CLSID_Dlldemo1) return *ppv = 0, CLASS_E_CLASSNOTAVAILABLE;
    CDlldemo1Factory* pFactory = new CDlldemo1Factory;
    HRESULT hr = pFactory->QueryInterface(riid, ppv);
   
if(hr) delete pFactory;
   
return hr;
}

Class factories, like CDlldemo1Factory, are only required to implement the IClassFactory interface.  Class factories may implement the IClassFactory2 interface if their components require lisencing.  In a DLL server, a class factory's AddRef, Release, and QueryInterface implementations are not fundamentally different from those of component classes like CDlldemo1.  The moduleCount global variable is also incremented or decremented by the factory's constructor and destructor.

With only two methods, IClassFactory is a very elegant inteface.  It is essentially a location and language transparent operator new.  The method IClassFactory::CreateInstance creates an instance of the corresponding COM object and returns a pointer to the interface identified by riid through the out parameter ppv.  If the component is to be used as an inner aggregation object, the pOuter argument holds a pointer to the outer object.  If the component doesn't support aggregation, it returns CLASS_E_NOAGGREGATION.  The CreateInstance implementation news an instance of CDlldemo1 and queries for the requested interface.  If the query fails, the object is deleted so as not to cause a leak.

IClassFactory::LockServer is an interesting story.   MSDN says about LockServer, "Called by the client of a class object to keep a server open in memory, allowing instances to be created more quickly."  However, only when no components are running will the DLL server let itself die (that is, when moduleCount is zero).  If the client has a pointer to a class factory, then an outstanding class factory exists, and the moduleCount is nonzero, so the server is already guaranteed not to unload itself.  Then what is the use of LockServer?  In a DLL server, none.  Our implementation simply returns S_OK, and no one is the wiser.  As we'll see in the next section, LockServer plays a role in EXE servers.

The final piece of code in the above listing is the DllGetClassObject export function definition.  This function is called by the COM runtime (as part of the CoGetClassObject implementation) when a component's class factory is requested.  The component of interest is specified with the rclsid argument.  Our server supports only the CLSID_Dlldemo1 component, so CLASS_E_CLASSNOTAVAILABLE is returned if the COM runtime asks for anything else.  DllGetClassObject returns a pointer to the interface identified by riid through the out parameter ppv (using the same scheme as presented in the IClassFactory::CreateInstance implementation).

The Dlldemo1 component is now complete.  Add the export functions DllCanUnloadNow and DllGetClassObject to your DEF file and compile.  Register the component with regsvr32.exe or the Tools->Register Component menu item in Developer Studio.  Your component is now ready to go!

vbdll1.gif (25307 bytes)

Figure 1

Figure 1 shows the Visual Basic form and source code for our test client.  To use our component, add its type library to the Object Browser through the References dialog box.  This test client uses both IDlldemo1 methods (HelloMessage and HelloMessage2) and its property (Message). 

vbdll2.gif (7668 bytes)

Figure 2

The component is put through its paces in Figure 2.   Let us rejoice in the goodness of the DLL server: everything works.

 

An EXE Server

Construction of a DLL server was a good, straightforward introduction to COM servers.  EXE servers have some interesting properties that make them a lot more fun to work with.  Let's briefly go over the basic mechanics of EXE servers before we jump into the source.

Like in-proc servers, local servers control their own lifetime and expose class factories to the COM runtime library.  Of course, the mechanisms by which they accomplish this are entirely different than those discussed in the previous section.  Because EXEs can't support export funtions, something like DllGetClassObject won't work with EXE servers.  Instead, COM provides a per-process class factory registration table.  Early in WinMain, the server creates class factories for every component it wants to expose and registers them in the table with the COM runtime call CoRegisterClassObject.  All clients can IClassFactory::CreateInstance the desired components through the class factory pointers stored in this table (assuming the factory is multiple-use).

EXE server lifetime management is seemingly straightforward: the EXE kills itself when it is no longer needed.  The EXE server maintains a count of the outstanding objects in the server.  When this counter is decremented to zero, the server waits a few seconds (to give other clients a chance to connect before it shuts itself down) then breaks the WinMain message loop, revokes the class factory pointers from the registration table, and returns. 

There is an ugly problem with what I've just described: the server will never unload itself.  Let's say the server creates a class factory, QueryInterfaces for IUnknown, and calls CoRegisterClassObject.  The global object counter (moduleCount) is now at least one because the class factory's constructor incremented it.  The reference count on the class factory is now at least two: the initial QI for IUnknown increments it to one, and CoRegisterClassObject AddRefs because it caches the class factory's pointer (which is one of the rules of COM - if a function caches a pointer for later use, it must AddRef).  So the client calls CoGetClassObject or CoCreateInstance and COM marshals the appropriate class factory pointer from the registration table to the client.  The client instantiates the COM class with IClassFactory::CreateInstance, and the server's moduleCount is incremented to two (care of the class's constructor).  Having no need for the class factory any longer, the client releases it.  This does not, however, destroy the class factory; COM AddRefed the class factory's interface pointer because it cached it.  The client does what it wants with the COM object and finally releases all of its interface pointers, which destroys the object and decrements the server's moduleCount.  Now since the client has no outstanding references to any of the server's objects, the server should destroy itself.  But the server doesn't know that it should destroy itself - moduleCount is still greater than zero.  The server needs to persuade COM to Release its class factory by calling CoRevokeClassObject, but this function is only called when the server dies (that is, when moduleCount is decremented to zero).  A true COMundrum.  Our solution?  Don't reference count the class factories or increment moduleCount in their constructors.

Now what happens?  The server creates an instance of each class factory and registers them with CoRegisterClassObjectmoduleCount remains zero.  The client grabs a class factory pointer with CoGetClassObject and creates the corresponding COM object with IClassFactory::CreateInstance.  The object's constructor increments the server's moduleCount to one.  The client plays with the component, and releases it when it's done.  The final release destroys the object, and the object's destructor decrements the server's count back down to zero.  The server says "Aha!  Zero!" and shuts itself down.  That works great.  But what if the client wants to cache a class factory pointer or guarantee that the server won't kill itself?  It calls IClassFactory::LockServer(TRUE), which increments the server's moduleCount.  Now the server can only shut itself down when it receives the corresponding unlock call IClassFactory::LockServer(FALSE)

Even with this scheme there is a potential for concern: the client could hold a class factory pointer (which it didn't LockServer) that goes bad when the server shuts down.  The COM runtime saves the day.  When a class factory pointer from the registration table is marshaled to the client by CoGetClassObject, COM builds an IPC proxy that implements IClassFactory.  This proxy is a legitimate, reference-counted object.  At the proxy's construction, COM calls IClassFactory::LockServer(TRUE) on the class factory, incrementing moduleCount.  When the client is done with the class factory (or rather, the class factory's proxy), it releases its pointer.  The reference count on the proxy drops to zero, and the proxy's destructor calls IClassFactory::LockServer(FALSE) on the real class factory.   This way, any outstanding references to the class factory (through a proxy) keep the server alive.  This scheme allows for a clever optimization: the COM-provided class factory can simply implement a trivial LockServer method.  If the client has a valid pointer to a class factory proxy, there is no way the server can free itself (as the proxy's constructor locked the server), so IClassFactory::LockServer on the proxy is unnecessary.  Latter versions of the COM runtime include this optimization.  The framers of the COMstitution certainly were a clever bunch.

exedemo1.cpp

#include <windows.h>
#include <atlbase.h>            // CComPtr, required by statreg.h
#include <statreg.h>            // CRegObject
#include "resource.h"
#include "exedemo1.h"
#include "exedemo1_i.c"

// registration described in previous article
void RegisterServer(wchar_t* module, bool reg) {
    CRegObject ro;
    ro.AddReplacement(L"Module", module);
    reg ? ro.ResourceRegister(module, IDR_REGISTRY, L"REGISTRY") :
        ro.ResourceUnregister(module, IDR_REGISTRY, L"REGISTRY");
}

// controls lifetime of server
// shutdownEvent is the synchronization object for moduleMonitorProc

long moduleCount;
HANDLE shutdownEvent;
DWORD mainThreadID;
bool moduleActivity;

// called by objects' destructors instead of InterlockedDecrement
void unlockModule()
{
    long count = InterlockedDecrement(&moduleCount);
    moduleActivity = true;
    if(!count) SetEvent(shutdownEvent);          // wake WaitForSingleObject
                                                 //  in moduleMonitorProc
}

DWORD _stdcall moduleMonitorProc(void*) {
    WaitForSingleObject(shutdownEvent, INFINITE);
    while(true) {
        DWORD waitRet = WAIT_OBJECT_0;
        while(waitRet == WAIT_OBJECT_0) {
            moduleActivity = false;
            waitRet = WaitForSingleObject(shutdownEvent, 3000);
        }
        if(moduleActivity || moduleCount) continue;
        PostThreadMessage(mainThreadID, WM_QUIT, 0, 0);
        return 0;
    }
}

We've reopened the exedemo1.cpp file from part I and added some more source.  RegisterServer remains unchanged, but unlockModule and moduleMonitorProc are new.  First let's meet our global data.  moduleCount holds the number of currently running objects that aren't class factories.  shutdownEvent is an initially unsignaled event that blocks the WaitForSingleObject call in moduleMonitorProc.  When unlockModule decrements moduleCount down to zero, shutdownEvent is signaled and moduleMonitorProc resumes execution.  The EXE server's lifetime is managed from a worker thread, and this worker thread needs some way to communicate with the main thread (the thread running WinMain) to facilitate termination.  PostThreadMessage is the mechanism of this communication, and mainThreadID holds the function's first argument (that is, the ID of the main thread).  The moduleActivity boolean reflects the wait state of the worker thread - false if moduleMonitorProc is waiting for shutdownEvent, true if it isn't.

Early in WinMain, a new thread enters moduleMonitorProc.   This function immediately is blocked by WaitForSingleObject.   The clients instantiate some COM objects and release them when they're no longer needed.  The objects' destructors invoke unlockModule, which decrements the module count.  Eventually all clients will have released their objects and moduleCount will decrement to zero.  When this happens, unlockModule sets shutdownEvent, and moduleMonitorProc wakes back up again.  Because shutdownEvent is an auto-reset event (you'll see the CreateEvent call later on), Windows resets it before returning from WaitForSingleObject.

The lifetime management thread now enters a while loop and again is blocked by WaitForSingleObject.  But this call times out at some arbitrary setting (in this server, three seconds).  If WaitForSingleObject doesn't return WAIT_OBJECT_0 here (i.e. shutdownEvent was signaled by unlockModule), the inner while loop is broken.  If moduleActivity is false (this is redundant - but redundancy, especially in space craft and COM local servers, is often a good thing) and the moduleCount still reads zero, WM_QUIT is posted to the main thread.

If during the three second wait for shutdownEvent a new COM object is instantiated, moduleCount will evaluate non-zero and the outer loop will continue.  If this COM object is destroyed during the three second wait, bringing moduleCount back down to zero, WaitForSingleObject will (prematurely) return WAIT_OBJECT_0.  Since shutdownEvent is auto-reset, the next time through the inner loop, WaitForSingleObject will return WAIT_TIMEOUT (assuming no new objects are created and destroyed during that three second interval).  This return value breaks the inner loop, moduleActivity and moduleCount are evaluated, and the quit message is posted.  No matter how things slice out, the server will live at least three seconds after all of its objects are released.  This gives other clients the chance to connect to the server before it restarts, preventing a wasteful termination and immediate re-launching of the process.  This is essentially the code that the ATL AppWizard generates to manage local server lifetimes.

from exedemo1.cpp

class CExedemo1 : public IExedemo1 {
    ULONG _ref;
    CComBSTR _bstrMessage;
public:
    CExedemo1() : _ref(0) {
        InterlockedIncrement(&moduleCount);
        _bstrMessage = L"This is the default message";
    }
    ~CExedemo1() { unlockModule(); }   // will SetEvent(shutdownEvents)
                                       //  if final unlock


    // from IUnknown
    virtual ULONG _stdcall AddRef() { return ++_ref; }
   
virtual ULONG _stdcall Release() {
        ULONG ret(--_ref);
        if(!ret) delete this;
       
return ret;
    }
   
virtual HRESULT _stdcall QueryInterface(REFIID riid, void** ppv) {
       
if(!ppv) return E_POINTER;
       
if(riid == IID_IUnknown) *ppv = this;
        else
if(riid == IID_IExedemo1) *ppv = this;
       
else return *ppv = 0, E_NOINTERFACE;
       
return AddRef(), S_OK;
    }

    // from IExedemo1
   
virtual HRESULT _stdcall HelloMessage() {
        char* ansiMessage = new
char[_bstrMessage.Length() + 1];
        WideCharToMultiByte(CP_ACP, 0, _bstrMessage.m_str,
            _bstrMessage.Length() + 1, ansiMessage,
            _bstrMessage.Length() + 1, 0, 0);
        MessageBox(0, ansiMessage, "HelloMessage() called",
            MB_OK | MB_ICONEXCLAMATION);
       
delete ansiMessage;
       
return S_OK;
    }
   
virtual HRESULT _stdcall HelloMessage2(BSTR bstrHi) {
       
char* ansiMessage = new char[SysStringLen(bstrHi) + 1];
        WideCharToMultiByte(CP_ACP, 0, bstrHi, SysStringLen(bstrHi) + 1,
            ansiMessage, SysStringLen(bstrHi) + 1, 0, 0);
        MessageBox(0, ansiMessage, "HelloMessage2() called",
            MB_OK | MB_ICONEXCLAMATION);
       
delete ansiMessage;
       
return S_OK;
    }
   
virtual HRESULT _stdcall get_Message(BSTR* pBstrHi) {
       
if(!pBstrHi) return E_POINTER;
        _bstrMessage.CopyTo(pBstrHi);
       
return S_OK;
    }
   
virtual HRESULT _stdcall put_Message(BSTR bstrHi) {
        _bstrMessage = bstrHi;
       
return S_OK;
    }
};

One of the really amazing things about COM is that objects can generally be implemented in the same way, regardless of it they run in-process, local, remote, or as an NT service (a service server is an EXE server with some funky additions).   CExedemo1's implementation is nearly identical to CDlldemo1's.   The only real change is the destructor's call to unlockModule.

Although the DLL and EXE object implementations are practically the same, the factory implementations are definately not:

from exedemo1.cpp
       
class CExedemo1Factory : public IClassFactory {
public:
    CExedemo1Factory() { }                              // don't lock here
    ~CExedemo1Factory() { }                             // don't unlock here
   
    // from IUnknown
   
virtual ULONG _stdcall AddRef() { return 2; }       // no need to AddRef
   
virtual ULONG _stdcall Release() { return 1; }      //  or Release
   
virtual HRESULT _stdcall QueryInterface(REFIID riid, void** ppv) {
       
if(!ppv) return E_POINTER;
       
if(riid == IID_IUnknown) *ppv = this;
        else
if(riid == IID_IClassFactory) *ppv = this;
       
else return *ppv = 0, E_NOINTERFACE;
       
return AddRef(), S_OK;
    }

    // from IClassFactory
   
virtual HRESULT _stdcall CreateInstance(IUnknown* pOuter, REFIID riid,
       
void** ppv) {
       
if(!ppv) return E_POINTER;
       
if(pOuter) return *ppv = 0, CLASS_E_NOAGGREGATION;
        CExedemo1* pExedemo1 = new CExedemo1;
        HRESULT hr = pExedemo1->QueryInterface(riid, ppv);
       
if(hr) delete pExedemo1;
       
return hr;
    }
   
virtual HRESULT _stdcall LockServer(BOOL fLock) {
        fLock ? InterlockedIncrement(&moduleCount) : unlockModule();
                            // LockServer is implemented here
       
return S_OK;
    }
};

Notice the complete lack of data members in the factory class (well, there is a pointer to the IClassFactory virtual table, but you can't see it).  Our reference counter is gone, and AddRef and Release just return constants.  Also, IClassFactory::LockServer is now implemented to increment the module count or invoke unlockModule, depending on the argument fLock.  If the client wants to cache a class factory, it must call both AddRef (to satisfy DLL server factories) and LockServer (for local server factories).  Both the QueryInterface and CreateInstance implementations are the same as those for CDlldemo1Factory.

int _stdcall WinMain(HINSTANCE hInstance, HINSTANCE, char* cmdLine, int) {

    // registration stuff covered in previous article
   
if(cmdLine) {
       
if(!stricmp(cmdLine, "-RegServer") ||
            !stricmp(cmdLine, "/RegServer")) {
            char ansiPath[MAX_PATH];
            GetModuleFileName(hInstance, ansiPath, MAX_PATH);
            wchar_t module[MAX_PATH];
            MultiByteToWideChar(CP_ACP, 0, ansiPath, lstrlen(ansiPath) + 1,
                module, MAX_PATH);
            RegisterServer(module, true);
            CComPtr<ITypeLib> pTypeLib;
            LoadTypeLib(module, &pTypeLib);
            HRESULT hr(RegisterTypeLib(pTypeLib, module, 0));
            return hr ? SELFREG_E_TYPELIB : S_OK;
        }
       
if(!stricmp(cmdLine, "-UnregServer") ||
            !stricmp(cmdLine, "/UnregServer")) {
           
char ansiPath[MAX_PATH];
            GetModuleFileName(hInstance, ansiPath, MAX_PATH);
           
wchar_t module[MAX_PATH];
            MultiByteToWideChar(CP_ACP, 0, ansiPath, lstrlen(ansiPath) + 1,
                module, MAX_PATH);           
            RegisterServer(module, false);
            HRESULT hr(UnRegisterTypeLib(LIBID_Exedemo1, 1, 0, 0, SYS_WIN32));
           
return hr ? SELFREG_E_TYPELIB : S_OK;
        }
    }

    // we must initialize COM for this process because
    //  no one else will do it for us

    CoInitialize(0);
    shutdownEvent = CreateEvent(0, false,
false, 0);
    mainThreadID = GetCurrentThreadId();
    DWORD moduleMonitorID;
    CreateThread(0, 0, moduleMonitorProc, 0, 0, &moduleMonitorID);
    DWORD exedemo1RegisterCookie;
    CExedemo1Factory factory;
   
    // expose the class factory
    HRESULT hr = CoRegisterClassObject(CLSID_Exedemo1, &factory,
        CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &exedemo1RegisterCookie);

    MSG msg;
    while(GetMessage(&msg, 0, 0, 0)) {     // WM_QUIT posted in
        TranslateMessage(&msg);            // moduleMonitorProc
        DispatchMessage(&msg);
    }
    CoRevokeClassObject(exedemo1RegisterCookie);
    return 0;
}

The registration code at the top of WinMain is covered in part I.  If the server is not asked to register itself, it registers its class factories and acts as a COM local server.  After initializing COM (COM has to be initialized in every process; DLL servers don't initialize COM because their clients do), shutdownEvent is created as an auto-reset event with an initial unsignaled value (the second parameter to CreateEvent controls manual-reset).  The global variable mainThreadID is set to the main thread's ID, and the worker thread is created and enters moduleMonitorProc.   Before entering the message loop, our server creates an instance of the class factory CExedemo1Factory on the stack (this is safe because with no reference count, it won't invoke delete on itself) and registers it with CoRegisterClassObject.   Notice the flag REGCLS_MULTIPLEUSE.  CoRegisterClassObject takes flags of the REGCLS enum as its fourth parameter.  A multiple use class factory allows any number of clients to connect to it.  Not only is this the most efficient setting, it allows different clients to communicate with each other through singleton COM objects.  You can also register your factory as REGCLS_SINGLEUSE.  With this setting, a new EXE is launched every time a client connects to the factory.  Finally, the server enters the message loop and waits until moduleCount drops to zero and WM_QUIT breaks the while loop.  CoRevokeClassObject removes the factory from the process's registration table and the server terminates. 

vbexe1.gif (24834 bytes)

Figure 3

Figure 3 shows the test client for our EXE server component.  Again, the component's type library was added to the Visual Basic Object Browser.  Note that the source code is line for line equivalent to that of Figure 1: COM objects really are location transparent.

vbexe2.gif (8025 bytes)

Figure 4

Lo and behold! it works.  Building a COM local server in raw C++ wasn't really that hard, was it?

vbexe3.gif (8169 bytes)

Figure 5

Here's a snapshot of my task manager (go ahead, laugh, it's Windows 98).  The 9x task manager displays all the running processes by filename unless they have created a visible window, in which case the window's name is listed.  Sure enough, our COM local server is listed in the task box by the name of its window, "HelloMessage2() called."

You can now write both in-process and local COM servers in raw C++.   That alone ranks you higher in eliteness than any lowely Wizard-user.  Keep reading The Journals of Captain COM for the best low-level COM programming information.  Until then, live by The Motto Of Captain COM:

I Came.  I Saw.  I Componentized.

Sean Baxter