In this article, you will learn how to write working COM components from scratch, and without a single macro.
Introduction
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 un-debuggable 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).
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:
#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
class CoCOMServer : public ICOMServer
{
public:
CoCOMServer();
~CoCOMServer();
virtual HRESULT __stdcall QueryInterface
(const IID& iid, void** ppv) ;
virtual ULONG __stdcall AddRef() ;
virtual ULONG __stdcall Release() ;
virtual HRESULT __stdcall Name(BSTR* objectname);
private:
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 three 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 ;
static long g_cServerLocks = 0 ;
CoCOMServer::CoCOMServer() : m_cRef(1)
{
InterlockedIncrement(&g_cComponents) ;
}
CoCOMServer::~CoCOMServer()
{
InterlockedDecrement(&g_cComponents) ;
}
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:
HRESULT __stdcall CoCOMServer::Name(BSTR* objectname)
{
if (objectname==NULL)
return ERROR_INVALID_PARAMETER;
CComBSTR dummy;
dummy.Append("hello world!");
*objectname = dummy.Detach();
return S_OK;
}
Our COM server is implemented so far. Of course, anyone 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 dictionary 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 outside. For instance, you can use the dumpbin /exports
command line (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 four exported functions:
STDAPI DllCanUnloadNow()
{
if ((g_cComponents == 0) && (g_cServerLocks == 0))
{
return S_OK ;
}
else
{
return S_FALSE ;
}
}
STDAPI DllGetClassObject(const CLSID& clsid,
const IID& iid,
void** ppv)
{
if (clsid != CLSID_CoCOMServer)
{
return CLASS_E_CLASSNOTAVAILABLE ;
}
CFactory* pFactory = new CFactory ;
if (pFactory == NULL)
{
return E_OUTOFMEMORY ;
}
HRESULT hr = pFactory->QueryInterface(iid, ppv) ;
pFactory->Release() ;
return hr ;
}
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;
}
STDAPI DllUnregisterServer()
{
HRESULT hr= UnregisterServer(CLSID_CoCOMServer,
g_szVerIndProgID,
g_szProgID,
LIBID_LibCOMServer) ;
if (SUCCEEDED(hr))
{
UnRegisterTypeLib( g_hModule, NULL);
}
return hr;
}
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 four 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 an odd reason, those who invented COM wanted to have intermediate objects to play with. So let there 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 plumbing 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 CFactory : public IClassFactory
{
public:
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
virtual ULONG __stdcall AddRef() ;
virtual ULONG __stdcall Release() ;
virtual HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv) ;
virtual HRESULT __stdcall LockServer(BOOL bLock) ;
CFactory() : m_cRef(1) {}
~CFactory() {;}
private:
long m_cRef ;
} ;
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 ;
}
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv)
{
if (pUnknownOuter != NULL)
{
return CLASS_E_NOAGGREGATION ;
}
CoCOMServer* pA = new CoCOMServer ;
if (pA == NULL)
{
return E_OUTOFMEMORY ;
}
HRESULT hr = pA->QueryInterface(iid, ppv) ;
pA->Release() ;
return hr ;
}
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 are 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 un-register 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;
HRESULT hr = CoCreateInstance( CLSID_CoCOMServer, NULL, CLSCTX_ALL,
IID_ICOMServer,
(void **)&p);
if (SUCCEEDED(hr))
{
BSTR message;
p->Name(&message);
CString szMessage;
AfxBSTR2CString(message, szMessage);
AfxMessageBox(szMessage);
::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 four 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.
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 three 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 to embarrass 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
class CoCOMServer : public ICOMServer
{
public:
CoCOMServer();
~CoCOMServer();
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
virtual ULONG __stdcall AddRef() ;
virtual ULONG __stdcall Release() ;
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);
virtual HRESULT __stdcall Name(BSTR* objectname);
private:
HRESULT LoadTypeInfo(ITypeInfo ** pptinfo,
const CLSID& libid, const CLSID& iid, LCID lcid);
long m_cRef ;
LPTYPEINFO m_ptinfo;
};
And now for the implementation. First of all, let's load the type-library (we actually load the .tlb file):
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;
hr = ::LoadRegTypeLib(libid, 1, 0, lcid, &ptlib);
if (FAILED(hr))
return hr;
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<ICOMServer*>(this) ;
}
else
{
*ppv = NULL ;
return E_NOINTERFACE ;
}
reinterpret_cast<IUnknown*>(*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();
*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 MS Word, 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 explicitly tagged [out,retval]
it's actually a result value, hence the assignment to the szname
variable.
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 have 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:
#include "stdafx.h"
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
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 going 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:
#include "stdafx.h"
#include <objbase.h> //
#include "automexeserver_i.h"
#include "automexeserverImpl.h"
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
char szUpperCommandLine[MAX_PATH];
strcpy (szUpperCommandLine, lpCmdLine);
strupr (szUpperCommandLine);
if (strstr (szUpperCommandLine, "UNREGSERVER"))
{
DllUnregisterServer();
return 0;
}
else if (strstr (szUpperCommandLine, "REGSERVER"))
{
DllRegisterServer();
return 0;
}
::CoInitialize(NULL);
DWORD nToken = CoEXEInitialize();
MSG msg;
while (GetMessage(&msg, 0, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
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 ourselves 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 that 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 to code the four 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);
}
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;
}
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 command line: 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).
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.
History
- 10th November, 2002: Initial version
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.