A simple yet debuggable COM skeleton code

 

This article and topic will probably inspire reluctance from code gurus. But, even after 7 years and a massive use in today's Windows-centric world, I often see people wondering the simplest questions about COM.

Microsoft introduced several years ago the ATL library, hoping that ready-to-use macros would simplify the COM development process. But in practice, the macros tend to obfuscate what the code really does and is expected to do. Furthermore, one obvious thing at the basis of this article is that ATL macros produce undebuggable code. Macros are expanded by the C/C++ preprocessor, which means it's virtually impossible to figure out what might be wrong in any ATL-based code.

That's why I shall in this article show how to write working COM components from scratch, and without a single macro. Hope you find it useful.

The remainder of this article provides three (hopefully reusable) sample COM implementations :

  • a simple COM dll
  • an automation-enabled COM dll
  • an automation-enabled COM exe
plus several working test environments (C/C++ and VB).

 

1. A simple COM dll

You'll find above a working simplecomserver.zip package which is the resulting code for the following steps.

Start VC6/7, create a new project called simplecomserver. Select the WIN32 dynamic library project wizard, and opt for the simple DLL project option.

So far, you should have a project with stdafx.h/cpp precompiled headers, plus simplecomserver.cpp with this code :
// simplecomserver.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
                     )
{
    return TRUE;
}
Now we are going to declare our COM interface. Let's create a new .idl file (simplecomserver.idl), and paste what is below into it :
import "wtypes.idl";

[
    uuid(6F818C55-E6AD-488b-9EB6-511C0CCC0612),
    version(1.0)
]
library LibCOMServer
{
	importlib("stdole32.tlb");
	importlib("stdole.tlb"); 

	[ uuid(7F24AABF-C822-4c18-9432-21433208F4DC), 
	  oleautomation 
	]
	interface ICOMServer : IUnknown
	{
		HRESULT Name([out] BSTR* objectname);
	}


	[ uuid(6AE24C34-1466-482e-9407-90B98798A712),
	  helpstring("COMServer object") 
	]
	coclass CoCOMServer
	{
		[default] interface ICOMServer;
	}
}

We have an interface inside an object (coclass keyword), itself part of a set of objects (there's only one here) belonging to a type-library (library keyword). Both interfaces, objects and type-libraries have ids that uniquely identify them : it's produced by a tool such as guidgen.exe (available from MSDEV / Tools menu).

Add simplecomserver.idl to your project files, right-click on it and compile it. This produces the type-library in the Debug folder. simplecomserver.tlb is a binary-compiled idl file, nothing else. The type library is what applications and languages are going to read and parse to extract interface names, method names, parameter qualifiers and so on at run-time. That's why the type library is such an important piece to worry about.

Since we provide a C/C++ implementation for this COM server, we are also going to ask the type-library compiler to produce an interface header to derive from. Right-click on simplecomserver.idl, select Settings, then from the MIDL tab provide the "Output header file name" field with the simplecomserver_i.h value. Compile the idl file again. This auto-generated header file is somewhat complex to read because it carries a lot of implementation details the COM library could not hide. But we don't really have to care about it. Let's just remember the header file is a class declaration we are going to implement. That's easy so far, so let's create a new header file, simplecomserverImpl.h, and provide it with this declaration :

#pragma once

// ICOMServer interface declaration ///////////////////////////////////////////
//
//

class CoCOMServer : public ICOMServer
{

	// Construction
public:
	CoCOMServer();
	~CoCOMServer();

	// IUnknown implementation
	//
	virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
	virtual ULONG __stdcall AddRef() ;
	virtual ULONG __stdcall Release() ;

	// ICOMServer implementation
	//
	virtual HRESULT __stdcall Name(/*out*/BSTR* objectname);


private:
	// Reference count
	long		m_cRef ;
};
If you compare this with what we declared in the .idl file a couple of minutes ago, you'll notice it looks much the same and is just expressed 100% with C/C++ syntax. We can also note 3 methods there from the IUnknown interface. Those methods help to manage the COM server life cycle by providing routing capabilities (QueryInterface), and safe reference counting (AddRef, Release). But there's no mystery or hidden trick, we are going to implement these methods right now. Let's create an implementation file simplecomserverImpl.cpp, and paste the code below :


#include "stdafx.h"

#include <objbase.h> // 
#include "simplecomserver_i.h"

#include <atlbase.h> // CComBSTR

#include "simplecomserverImpl.h"

static long g_cComponents = 0 ;     // Count of active components
static long g_cServerLocks = 0 ;    // Count of locks


//
// Constructor
//
CoCOMServer::CoCOMServer() : m_cRef(1)
{ 
	InterlockedIncrement(&g_cComponents) ; 
}

//
// Destructor
//
CoCOMServer::~CoCOMServer() 
{ 
	InterlockedDecrement(&g_cComponents) ; 
}

//
// IUnknown implementation
//
HRESULT __stdcall CoCOMServer::QueryInterface(const IID& iid, void** ppv)
{    
	if (iid == IID_IUnknown || iid == IID_ICOMServer)
	{
		*ppv = static_cast<ICOMServer*>(this) ; 
	}
	else
	{
		*ppv = NULL ;
		return E_NOINTERFACE ;
	}
	reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
	return S_OK ;
}

ULONG __stdcall CoCOMServer::AddRef()
{
	return InterlockedIncrement(&m_cRef) ;
}

ULONG __stdcall CoCOMServer::Release() 
{
	if (InterlockedDecrement(&m_cRef) == 0)
	{
		delete this ;
		return 0 ;
	}
	return m_cRef ;
}
This code skeleton is enough for every interface you may implement until the end of time. Of course, now we are going to add a custom implementation of ours :
//
// ICOMServer implementation
//
HRESULT __stdcall CoCOMServer::Name(/*out*/BSTR* objectname)
{
	if (objectname==NULL)
		return ERROR_INVALID_PARAMETER;

	CComBSTR dummy;
	dummy.Append("hello world!");

	*objectname = dummy.Detach(); // Detach() returns an allocated BSTR string

	return S_OK;
}
Our COM server is implemented so far. Of course who heard of COM in his life knows that a COM server needs to be registered before being used. Indeed the client application, whatever application it is, is going to use indirectly the COM library which in turns looks up a dictionnary of known COM servers in the registry (the HKEY_CLASSES_ROOT \ CLSID key). So what we are going to do is make sure that our build process also automatically registers our COM server. This is going to require a few steps and you'd better attach your seat belt! Of course, the fine thing is that we will never be required to create the many registry keys by hand, I mean by raw and stupid registry-related code. There would be some work there since we need to register the COM object itself, the type-library, and the interface. Let's begin by adding a few exported functions. Those exported functions are available from outside and allow the registration tool (ever heard of regsvr32.exe ?) to request for registration. That's the DllRegisterServer function. Let's add a new type of file : simplecomserver.def and paste what's below in it :

LIBRARY      "simplecomserver"

DESCRIPTION  'Proxy/Stub DLL'

EXPORTS
    DllCanUnloadNow         @1  PRIVATE
    DllGetClassObject       @2  PRIVATE
    DllRegisterServer       @3  PRIVATE
    DllUnregisterServer     @4  PRIVATE
A .def file just tells the linker to let the listed functions be available from the outside. For instance, you can use the dumpbin /exports commandline (MSDEV tool, requires the MSDEVDIR in the path), or even the Dependency Walker (another MSDEV tool) to see them as soon as this file is added to the project files, and the project built. Let's provide implementation for the 4 exported functions :
///////////////////////////////////////////////////////////
//
// Exported functions
//

//
// Can DLL unload now?
//
STDAPI DllCanUnloadNow()
{
	if ((g_cComponents == 0) && (g_cServerLocks == 0))
	{
		return S_OK ;
	}
	else
	{
		return S_FALSE ;
	}
}

//
// Get class factory
//
STDAPI DllGetClassObject(const CLSID& clsid,
                         const IID& iid,
                         void** ppv)
{
	// Can we create this component?
	if (clsid != CLSID_CoCOMServer)
	{
		return CLASS_E_CLASSNOTAVAILABLE ;
	}

	// Create class factory.
	CFactory* pFactory = new CFactory ;  // Reference count set to 1
	                                     // in constructor
	if (pFactory == NULL)
	{
		return E_OUTOFMEMORY ;
	}

	// Get requested interface.
	HRESULT hr = pFactory->QueryInterface(iid, ppv) ;
	pFactory->Release() ;

	return hr ;
}

//
// Server registration
//
STDAPI DllRegisterServer()
{
    HRESULT hr= RegisterServer(g_hModule, 
                               CLSID_CoCOMServer,
                               g_szFriendlyName,
                               g_szVerIndProgID,
                               g_szProgID,
                               LIBID_LibCOMServer) ;
    if (SUCCEEDED(hr))
    {
        RegisterTypeLib( g_hModule, NULL);
    }
    return hr;
}


//
// Server unregistration
//
STDAPI DllUnregisterServer()
{
    HRESULT hr= UnregisterServer(CLSID_CoCOMServer,
                                 g_szVerIndProgID,
                                 g_szProgID,
                                 LIBID_LibCOMServer) ;
    if (SUCCEEDED(hr))
    {
        UnRegisterTypeLib( g_hModule, NULL);
    }
    return hr;
}



///////////////////////////////////////////////////////////
//
// DLL module information
//
BOOL APIENTRY DllMain(HANDLE hModule,
                      DWORD dwReason,
                      void* lpReserved)
{
  if (dwReason == DLL_PROCESS_ATTACH)
  {
    g_hModule = (HMODULE)hModule ;
  }
  return TRUE ;
}

A few notes here : first of all the 4 functions are prefixed with STDAPI, that's just to tell the linker to export them without any C++ mangling (otherwise @ and numbers would appear around the function names, somewhat unnecessary as for now). We also provide a new DllMain implementation replacing the default code provided by the class wizard a couple of minutes ago : now you need to remove what the class wizard generated for us in simplecomserver.cpp (otherwise the linker would complain of a duplicate DllMain() implementation).

DllRegisterServer() and DllUnregisteServer() are the entry points for the registration. At this point, we are not going to dive much into details, let's just remember that the type-library knows the ins and outs of the COM server and will perform most of the actual registration itself.

And we have DllGetClassObject(). This one is the entry-point used by the outside to create an instance of our COM server. For odd reason, those who invented COM wanted to have intermediate objects to play with. So let their be the class factory. This object behaves much like a COM object itself, although it doesn't have an associated idl file. DllGetClassObject() is called by the COM plumbering on behalf of the outside application, and expects in return a valid pointer to an IClassFactory interface. This interface (again an interface is a simple class) implements the CreateInstance() construction method, and that one actually instantiates our COM server. As you can see, so far there's not much to worry about the class factory. We just copy/paste this code in simplecomserverImpl.cpp :

///////////////////////////////////////////////////////////
//
// Class factory
//
class CFactory : public IClassFactory
{
public:
  // IUnknown
  virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;         
  virtual ULONG   __stdcall AddRef() ;
  virtual ULONG   __stdcall Release() ;

  // Interface IClassFactory
  virtual HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
                                           const IID& iid,
                                           void** ppv) ;
  virtual HRESULT __stdcall LockServer(BOOL bLock) ; 

  // Constructor
  CFactory() : m_cRef(1) {}

  // Destructor
  ~CFactory() {;}

private:
  long m_cRef ;
} ;


//
// Class factory IUnknown implementation
//
HRESULT __stdcall CFactory::QueryInterface(const IID& iid, void** ppv)
{    
  if ((iid == IID_IUnknown) || (iid == IID_IClassFactory))
  {
    *ppv = static_cast<IClassFactory*>(this) ; 
  }
  else
  {
    *ppv = NULL ;
    return E_NOINTERFACE ;
  }
  reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
  return S_OK ;
}

ULONG __stdcall CFactory::AddRef()
{
  return InterlockedIncrement(&m_cRef) ;
}

ULONG __stdcall CFactory::Release() 
{
  if (InterlockedDecrement(&m_cRef) == 0)
  {
    delete this ;
    return 0 ;
  }
  return m_cRef ;
}

//
// IClassFactory implementation
//
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter,
                                           const IID& iid,
                                           void** ppv) 
{
  // Cannot aggregate.
  if (pUnknownOuter != NULL)
  {
    return CLASS_E_NOAGGREGATION ;
  }

  // Create component.
  CoCOMServer* pA = new CoCOMServer ;
  if (pA == NULL)
  {
    return E_OUTOFMEMORY ;
  }

  // Get the requested interface.
  HRESULT hr = pA->QueryInterface(iid, ppv) ;

  // Release the IUnknown pointer.
  // (If QueryInterface failed, component will delete itself.)
  pA->Release() ;
  return hr ;
}

// LockServer
HRESULT __stdcall CFactory::LockServer(BOOL bLock) 
{
  if (bLock)
  {
    InterlockedIncrement(&g_cServerLocks) ; 
  }
  else
  {
    InterlockedDecrement(&g_cServerLocks) ;
  }
  return S_OK ;
}
Guess what, we are done !

 

The registration is either performed by an explicit regsvr32 <path>\Debug\simplecomserver.dll shell command, or by adding a Custom Build Step in your project.

Once the registration has succeeded, you can look up the new keys in the registry :
+ HKEY_CLASSES_ROOT

  +  COMServer.object
    +  CLSID = {6AE24C34-1466-482e-9407-90B98798A712}

  +  CLSID
    +  {6AE24C34-1466-482e-9407-90B98798A712} = "COMServer object"
      +  InProcServer32 = <path>\simplecomserver.dll

  +  Interface
    +  {7F24AABF-C822-4c18-9432-21433208F4DC} = "ICOMServer"
      + TypeLib = {6F818C55-E6AD-488B-9EB6-511C0CCC0612}

  +  TypeLib
    + {6F818C55-E6AD-488b-9EB6-511C0CCC0612}
      + 1.0
        + 0
          + win32 = <path>\simplecomserver.tlb

I hope all this registry mess is clearer to you now. Basically you can see that registering a COM component is just a matter of adding the IDs of interfaces, objects, and type-libraries in the right places. Again, no hidden trick here.

For any reason, issues may occur while performing the registration and you may not get the successful "DllRegisterServer in simplecomserver.dll succeeded." message. But we don't have to worry, here is the steps to debug the registration :
  • in the DllMain() implementation, add a call to DllRegisterServer(). Doing so, each time the DLL is loaded, DllRegisterServer() will be called. Remember that's a temporary change, if you don't remove this call when registration seems to work fine, what it will do will be self-registration any time the DLL is loaded.
  • put a breakpoint inside DllRegisterServer()
  • start debug, and browse for any launch process to start with. In fact we are telling the debugger to start a process and automatically load our DLL.
  • debug the registration code.

 

If you want to unregister your component, just add the -u option in the regsvr32 shell command. As a result, DllUnregisterServer() will be called instead.

Anyone interested to test the COM server with a C/C++ client application, here is the code to do so (see the TestSimplecomserver.dsp project in the zip package) :
#include <atlbase.h>
#include "..\simplecomserver\simplecomserver_i.h" // interface declaration
#include "..\simplecomserver\simplecomserver_i.c" // IID, CLSID


::CoInitialize(NULL);

ICOMServer *p = NULL;

// create an instance
HRESULT hr = CoCreateInstance(	CLSID_CoCOMServer, NULL, CLSCTX_ALL,
                                IID_ICOMServer, 
                                (void **)&p);
if (SUCCEEDED(hr))
{
    // call the (only) method exposed by the main interface
    //
    BSTR message;
    p->Name(&message);

    // if everything went well, message holds the returned name
    //
    // ...
    CString szMessage;
    AfxBSTR2CString(message, szMessage);
    AfxMessageBox(szMessage);

    // don't forget to release the stuff
    ::SysFreeString(message);
}

p->Release();

::CoUninitialize();
The limitations of our COM server ?
  • applications with built-in automation languages (VB, Perl, Python, ...) cannot yet get to work with it. In fact, we must derive our interface from IDispatch instead of IUnknown, and provide a default implementation for 4 methods. We are going to see this in the next section.
  • it's a DLL. To make a COM server live behind the boundaries of a separate .EXE, we need some extra code. We are going to see this in the next section.

 

2. An automation COM dll

We are not going to build the automation-enabled COM dll from scratch since it's only a minor change over the simple COM server dll. However, you can as an exercise try to do so.

The resulting code is provided in the automcomserver.zip package.

What we are going to do is replace the IUnknown interface support by IDispatch support (which itself inherits IUnknown, this is the reason why the 3 methods QueryInterface(), AddRef() and Release() won't go away). But then why ? basically the IDispatch interface provides a convenient programmatic way to parse the type-library, and list the method names exposed by an anonymous IDispatch-derived interface. This is aimed to provide what's known as late binding. In other words, thanks to late binding a client application does not need anymore to statically link with an interface it uses. When, in the simple COM server dll, we include the simplecomserver_i.h interface declaration, we are in fact providing the compiler a static function vtable. This is not always good. Late binding allows to retrieve the position of an interface function by giving its name. In the long term, this provides software developers with a versatile binding system. This service is provided by IDispatch, a sort of pointer to the type-library.

But that's not enough, what about the parameters ? Automation languages are supposed to be able to guess method params on-the-fly at design-time, or even only at run-time. At design time, the problem is solved easily. In fact, when you registered your COM server, you registered the type-library as well, thus any automation-enabled language can read it on your behalf, extract all objects, interfaces, methods, and params, and then expose it through intellisense (for instance). If you are using VB, you typically add the type-library with the Tools \ References menu. Then the object browser lists all the things mentioned. So we are able to call methods discovered on-the-fly at design-time, but what about run-time ? At run-time, the automation engine needs to perform type binding, and that's what the other methods exposed by IDispatch are for. They provide entry points to the type-library and to an underlying API exposed by the operating system allows to extract all tiny details of your parameters : are they [in] ?, are they BSTR ?, ...

Although all of this sounds a bit scary, we are not going embarass ourselves with the details. In fact, most of the IDispatch implementation will be a default implementation already provided by one of the COM libraries. There we go :

So let's take back the .idl interface, and make the following changes on it :

  • Replace IUnknown by IDispatch, so to reflect the ICOMServer interface now supports automation
  • Add the [id(1)] prefix at the left of the Name method signature. This id maps the method with an index in a vtable, and is used to discover methods.
The idl interface should now look like this :
import "wtypes.idl";

[
    uuid(6F818C55-E6AD-488b-9EB6-511C0CCC0612),
    version(1.0)
]
library LibCOMServer
{
	importlib("stdole32.tlb");
	importlib("stdole.tlb"); 

	[ uuid(7F24AABF-C822-4c18-9432-21433208F4DC),
   	  dual,
	  oleautomation 
	]
	interface ICOMServer : IDispatch
	{
		[id(1)] HRESULT Name([out, retval] BSTR* objectname);
	}


	[ uuid(6AE24C34-1466-482e-9407-90B98798A712),
	  helpstring("COMServer object") 
	]
	coclass CoCOMServer
	{
		[default] interface ICOMServer;
	}
}

If you try to build the project now, you'll get errors since we don't provide implementation yet for IDispatch although we derive from it. So let it be :


#pragma once

// ICOMServer interface declaration ///////////////////////////////////////////
//
//

class CoCOMServer : public ICOMServer
{

  // Construction
public:
  CoCOMServer();
  ~CoCOMServer();

  // IUnknown implementation
  //
  virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
  virtual ULONG __stdcall AddRef() ;
  virtual ULONG __stdcall Release() ;

  //IDispatch implementation
  virtual HRESULT __stdcall GetTypeInfoCount(UINT* pctinfo);
  virtual HRESULT __stdcall GetTypeInfo(UINT itinfo, LCID lcid, ITypeInfo** pptinfo);
  virtual HRESULT __stdcall GetIDsOfNames(REFIID riid, 
            LPOLESTR* rgszNames, UINT cNames,
            LCID lcid, DISPID* rgdispid);
  virtual HRESULT __stdcall Invoke(DISPID dispidMember, REFIID riid,
            LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult,
            EXCEPINFO* pexcepinfo, UINT* puArgErr);


  // ICOMServer implementation
  //
  virtual HRESULT __stdcall Name(/*out*/BSTR* objectname);


private:

  HRESULT LoadTypeInfo(ITypeInfo ** pptinfo, const CLSID& libid, const CLSID& iid, LCID lcid);

  // Reference count
  long          m_cRef ;
  LPTYPEINFO    m_ptinfo; // pointer to type-library
};

And now for the implementation. First of all let's load the type-library (we actually load the .tlb file) :

//
// Constructor
//
CoCOMServer::CoCOMServer() : m_cRef(1)
{ 
  InterlockedIncrement(&g_cComponents) ; 

  m_ptinfo = NULL;
  LoadTypeInfo(&m_ptinfo, LIBID_LibCOMServer, IID_ICOMServer, 0);
}

HRESULT CoCOMServer::LoadTypeInfo(ITypeInfo ** pptinfo, const CLSID &libid, const CLSID &iid, LCID lcid)
{
  HRESULT hr;
  LPTYPELIB ptlib = NULL;
  LPTYPEINFO ptinfo = NULL;

  *pptinfo = NULL;

  // Load type library.
  hr = ::LoadRegTypeLib(libid, 1, 0, lcid, &ptlib);
  if (FAILED(hr))
    return hr;

  // Get type information for interface of the object.
  hr = ptlib->GetTypeInfoOfGuid(iid, &ptinfo);
  if (FAILED(hr))
  {
    ptlib->Release();
    return hr;
  }

  ptlib->Release();
  *pptinfo = ptinfo;
  return NOERROR;
}

We also allow the outside to request the IDispatch interface as we now fully support it :

HRESULT __stdcall CoCOMServer::QueryInterface(const IID& iid, void** ppv)
{    
  if (iid == IID_IUnknown || iid == IID_ICOMServer || iid == IID_IDispatch)
  {
    *ppv = static_cast(this) ; 
  }
  else
  {
    *ppv = NULL ;
    return E_NOINTERFACE ;
  }
  reinterpret_cast(*ppv)->AddRef() ;
  return S_OK ;
}

And we provide a default implementation for the IDispatch interface itself :

HRESULT __stdcall CoCOMServer::GetTypeInfoCount(UINT* pctinfo)
{
  *pctinfo = 1;
  return S_OK;
}

HRESULT __stdcall CoCOMServer::GetTypeInfo(UINT itinfo, LCID lcid, ITypeInfo** pptinfo)
{
  *pptinfo = NULL;

  if(itinfo != 0)
    return ResultFromScode(DISP_E_BADINDEX);

  m_ptinfo->AddRef();      // AddRef and return pointer to cached
                           // typeinfo for this object.
  *pptinfo = m_ptinfo;

  return NOERROR;
}

HRESULT __stdcall CoCOMServer::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames,
		LCID lcid, DISPID* rgdispid)
{
  return DispGetIDsOfNames(m_ptinfo, rgszNames, cNames, rgdispid);
}

HRESULT __stdcall CoCOMServer::Invoke(DISPID dispidMember, REFIID riid,
		LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult,
		EXCEPINFO* pexcepinfo, UINT* puArgErr)
{
  return DispInvoke(
               this, m_ptinfo,
               dispidMember, wFlags, pdispparams,
               pvarResult, pexcepinfo, puArgErr); 
}

Build the project, and register the component.

We are done, again ! This COM component supports automation languages, early binding and late binding.

 

Let's play now. Run MSWord, then go to the Visual Basic Editor, and copy/paste this code snippet :

Sub Macro1()

  Dim obj As ICOMServer
  Set obj = CreateObject("COMServer.object")
  Dim szname As String
  szname = obj.Name ' explicit Name([out,retval] BSTR*) method call
  MsgBox szname

End Sub

Before you run it, don't forget to go to Tools \ References and add a reference to the automcomserver.tlb type-library.

The reason why we don't pass a string as input parameter, unlike what the .idl interface suggests, is that when a param is explicitely tagged [out,retval] it's actually a result value, hence the assignment to the szname variable.

 

3. An automation COM exe

For many reasons it's better to run a COM component behind a separate process. Process isolation for security and performance reasons are just to name a few. But that's where things start to get a bit trickier. Indeed, for the end user a .exe COM server will be used exactly the same way as if it was a .dll : CoCreateInstance, QueryInterface, method calls, Release. But the developers has to pay all the implementation details.

We are going to reuse our code, and add a message pump, and register the class object to another table known by the COM library, and used for processes only.

The resulting code is provided in the automexeserver.zip package.

Let's create a new project, Win32 application this time instead Win32 dynamic library and call it automexeserver. Make sure you get this in automexeserver.cpp :

// automexeserver.cpp : Defines the entry point for the application.
//

#include "stdafx.h"

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
 	// TODO: Place code here.

	return 0;
}

Now copy, rename, and add all these files to this project :

  • automcomserver.idl ==> automexeserver.idl
  • automcomserverImpl.h ==> automexeserverImpl.h
  • automcomserverImpl.cpp ==> automexeserverImpl.cpp
  • Registry.h .cpp ==> Registry.h .cpp (no change needed here)

Of course, you need to make a few changes internally in the files so to reflect the new automexeserver name : you need to replace the #include statements that were referencing automcomserver headers, and you need to go to the Project Settings and change the MIDL options for automexeserver.idl : fill the "Output header filename" field with automexeserver_i.h.

As you have probably guessed by now, we don't need automcomserver.def anymore, which was used to declare the exported functions. In fact, a process does not need to export any function since the method call used will be LPC/RPC, not the simple in-process method call we've seen so far. No need to be frightened, we are not giving to dive into the marshalling frenzy. So relax.

It's ok to build the project, but it won't work as expected yet.

First of all, let's add some muscles to the application entry-point. Here we are going to initialize ourselves against the internal table of COM processes, and then we are going through breakable message pump. automexeserver.cpp should reflect exactly this :

// automexeserver.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include <objbase.h> // 
#include "automexeserver_i.h"
#include "automexeserverImpl.h"

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{

  // register/unregister server on demand
  //
  char szUpperCommandLine[MAX_PATH];
  strcpy (szUpperCommandLine, lpCmdLine); // copy command line and work with it.
  strupr (szUpperCommandLine);
  if (strstr (szUpperCommandLine, "UNREGSERVER"))
  {
    DllUnregisterServer();
    return 0;
  }
  else if (strstr (szUpperCommandLine, "REGSERVER"))
  {
    DllRegisterServer();
    return 0;
  }


  // initialize the COM library
  ::CoInitialize(NULL);

  // register ourself as a class object against the internal COM table
  // (this has nothing to do with the registry)
  DWORD nToken = CoEXEInitialize();


  // -- the message poooommmp ----------------
  //
  // (loop ends if WM_QUIT message is received)
  //
  MSG msg;
  while (GetMessage(&msg, 0, 0, 0) > 0) 
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
		

  // unregister from the known table of class objects
  CoEXEUninitialize(nToken);

  // 
  ::CoUninitialize();


  return 0;
}

A few remarks are required :

  • As you can see, the command line used to start the process is checked against a possible /regserver or /unregserver argument. Indeed, COM processes are not registered with the standard regsvr32 command line. The fact that we have DllRegisterServer() and DllUnregisterServer() should not be misunderstood. We have quite handy registry helpers that we used since the beginning. So it would be stupid not to use them again. But the Dll prefix does not mean we are registering a DLL. In fact, the almost only difference in the registration is we have at some point a LocalServer32 registry key instead of InProcServer32.
  • Then we register ourself in the internal COM class object table. (used by the R.O.T.).
  • Then there is the message pump, waiting for a WM_QUIT message, while processing all other messages along the way. It's important to note this WM_QUIT message is sent by the operating system whenever you kill the process by hand. And we are going to mimic this to kill ourselves whenever the client application does not need us anymore.

To finish the implementation, we provide the code the 4 mentioned functions in automexeserverImpl.cpp, namely DllRegisterServer(), DllUnregisterServer(), CoEXEInitialize() and CoEXEUninitialize() :


CFactory gClassFactory;



DWORD CoEXEInitialize()
{
  DWORD nReturn;

  HRESULT hr=::CoRegisterClassObject(CLSID_CoCOMServer,
                                     &gClassFactory,
                                     CLSCTX_SERVER, 
                                     REGCLS_MULTIPLEUSE, 
                                     &nReturn);

  return nReturn;
}

void CoEXEUninitialize(DWORD nToken)
{
  ::CoRevokeClassObject(nToken);
}





//
// Server registration
//
STDAPI DllRegisterServer()
{

  g_hModule = ::GetModuleHandle(NULL);

  HRESULT hr= RegisterServer(g_hModule, 
                             CLSID_CoCOMServer,
                             g_szFriendlyName,
                             g_szVerIndProgID,
                             g_szProgID,
                             LIBID_LibCOMServer) ;
  if (SUCCEEDED(hr))
  {
    RegisterTypeLib( g_hModule, NULL);
  }
  return hr;
}


//
// Server unregistration
//
STDAPI DllUnregisterServer()
{

  g_hModule = ::GetModuleHandle(NULL);

  HRESULT hr= UnregisterServer(CLSID_CoCOMServer,
                               g_szVerIndProgID,
                               g_szProgID,
                               LIBID_LibCOMServer) ;
  if (SUCCEEDED(hr))
  {
    UnRegisterTypeLib( g_hModule, NULL);
  }
  return hr;
}

Let us not forget that we need to kill ourselves when the client application tells us to. Let's update the CoCOMServer::Release() implementation :

ULONG __stdcall CoCOMServer::Release() 
{
  if (InterlockedDecrement(&m_cRef) == 0)
  {
    delete this ;
    ::PostMessage(NULL,WM_QUIT,0,0);
    return 0 ;
  }
  return m_cRef ;
}

Now you can build this project. Don't forget the shell commandline : automexeserver.exe /regserver before you use the COM server. You can test that one against the VB code snippet for instance (don't remember to reference the type-library, now it's automexeserver.tlb).

Update : Embedding the .tlb file right in your dll or executable : it's perfectly doable, just add the following statement in your .rc file, and make sure to add the .rc file in your .dsp project :

1 TYPELIB "automexeserver.tlb"

Alternatively, you can also use VC7 which if I remember has a flag in the project properties. You can also directly add your .tlb file in your project files and have it recognized by the compiler and added to your binary.

If you want to test the component using a C/C++ client application, use the TestAutomexeserver.dsp project for instance (the only difference with TestSimplecomserver.dsp is the inclusion of the proper interface header files. Experience shows that if you include wrong files, then at run-time a GPF is almost guaranteed!).

And we are done, finally !! Was it that hard ?

 

This article has provided you three real world code samples reflecting the 3 kind of code you need whenever you come nearby the COM frontier.

Hope you find them useful.

 

Stéphane Rodriguez. Nov 10, 2002.

Home
Blog