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 :
1. A simple COM dllYou'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 , 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 PRIVATEA .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
And we have /////////////////////////////////////////////////////////// // // 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 + 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 :
If you want to unregister your component, just add the -u option in the regsvr32 shell command. As a result, #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 ?
2. An automation COM dllWe 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 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 :
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 #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 HRESULT __stdcall CoCOMServer::QueryInterface(const IID& iid, void** ppv) { if (iid == IID_IUnknown || iid == IID_ICOMServer || iid == IID_IDispatch) { *ppv = static_cast And we provide a default implementation for the 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 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
3. An automation COM exeFor 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 : 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 :
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 As you have probably guessed by now, we don't need 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 :
To finish the implementation, we provide the code the 4 mentioned functions in 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 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 : 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 |