Step by Step COM Tutorial

top

Introduction

For me , understanding COM (Component Object Model) has been no less than an odyssey. I believe that every programmer who wishes to understand the basic principles behind COM, must write atleast one simple COM object using plain C++ , i.e. without the aid of templates and macros that comes along with MFC/ATL. In this article I present the guidelines for creating simple COM objects from first principles. The components should be usable by both VC/VB clients.

As an exercise we will attempt to design a COM component that will implement a hypothetical super-fast addition algorithm. The component must take in two parametes of long data type, and return to the user another long parameter that will be an outcome of our addition algorithm.We will begin with designing the interface.

top

Interface

The interface of a COM object does not talk about the actual implementation, but the method signatures that will be used by others to communicate with the COM object. We will name our interface as IAdd. The declaration for this interface will be done using the Interface Definition Language (IDL). IDL is a language used for defining function signatures, in a format that is independent of the programming language, and this helps the RPC infrastructure to pack, ship and unpack parameters from one computer to another. In our interface IAdd, we will have methods SetFirstNumber and SetSecondNumber which will be used for passing the parameters for addition. There will be another method , DoTheAddition, that will actually do the addition and give back to the client the result.

top

Step 1:
Create a new Win32 DLL project (AddObj say). Create all the subsequent files in this folder. Create a blank file and keyin the following contents. Save it as IAdd.idl. The interface identifiers have been generated using the tool uuidgen.exe.

import "unknwn.idl";

[
object,
uuid(1221db62-f3d8-11d4-825d-00104b3646c0),
helpstring("interface  IAdd is used for implementing a super-fast addition Algorithm")
]

interface IAdd : IUnknown
    {
    HRESULT     SetFirstNumber(long nX1);

    HRESULT     SetSecondNumber(long nX2);
    
    HRESULT     DoTheAddition([out,retval] long *pBuffer);
    };

[
uuid(3ff1aab8-f3d8-11d4-825d-00104b3646c0),
helpstring("Interfaces for Code Guru algorithm implementations .")
]
library CodeGuruMathLib
    {
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

    interface IAdd;
    }


top

Step 2:
Compile the file IAdd.idl using the command line compiler MIDL.exe (note:midl.exe ships with VC++ and incase of any path problems for midl, you may need to fix your path variable settings )
Upon compilation the following files will be generated:

IAdd.h Contains the C++ style interface declarations.
dlldata.c Contains code for proxy DLL. Useful when invoking the object on a different process/computer.
IAdd.tlb Binary file , with a well defined format that completely describes our interface IAdd along with all it's methods. This file is to be distributed to all the clients of our COM component.
IAdd_p.c Contains marshalling code for proxy DLL. Useful while invoking the object on a different process/computer.
IAdd_i.c Contains the interface IIDs

top

Step 3:
We will create the COM object. Create a new file (AddObj.h), delclare a C++ class , name this class CAddObj. Derive this class from the interface IAdd (file IAdd.h). Remember that , IAdd derives from IUnknown, which is also a abstract base class. Therefore we will have to declare all the methods for the abstract base classes IAdd as well as IUnknown.

///////////////////////////////////////////////////////////
//
//AddObj.h
//Contains the C++ class declarations for implementing the IAdd
//interfaces
//
#include    "IAdd.h"
extern long g_nComObjsInUse;
class CAddObj : 
        public IAdd
    {
    public:

    //IUnknown interface 
    HRESULT __stdcall QueryInterface(
                                REFIID riid , 
                                void **ppObj);
    ULONG   __stdcall AddRef();
    ULONG   __stdcall Release();

    //IAdd interface
    HRESULT __stdcall SetFirstNumber( long nX1);
    HRESULT __stdcall SetSecondNumber( long nX2);
    HRESULT __stdcall DoTheAddition( long *pBuffer);

    private:

    long m_nX1 , m_nX2; //operands for addition
    long m_nRefCount;   //for managing the reference count
    };

///////////////////////////////////////////////////////////

top

Step 4:
We will provide implementations for the all methods of the IAdd interface. Create a new file (AddObj.cpp) and implement the method code here.

///////////////////////////////////////////////////////////
//
//AddObj.cpp
//Contains the method  implementations of the IAdd interface
//interfaces
//

#include    <objbase.h>

#include    "AddObj.h"
#include    "IAdd_i.c"

HRESULT __stdcall CAddObj::SetFirstNumber( long nX1)
    {
    m_nX1=nX1;
    if (m_bIsLogEnabled) WriteToLog("Junk");
    return S_OK;
    }

HRESULT __stdcall CAddObj::SetSecondNumber( long nX2)
    {
    m_nX2=nX2;
    return S_OK;
    }

HRESULT __stdcall CAddObj::DoTheAddition( long *pBuffer)
    {
    *pBuffer =m_nX1 + m_nX2;

    return S_OK;
    }

top

Step 5:
IUnknown methods need to be implemented. We will implement the 3 mandatory methods (AddRef, Release and QueryInterface) in the same file AddObj.cpp. The private member m_nRefCount is used for maintainig the object life time. m_nRefCount is not decremented/incremented directly, instead we do it in a thread safe way, using the API InterlockedIncrement and InterlockedDecrement


HRESULT __stdcall CAddObj::QueryInterface(
                                    REFIID riid , 
                                    void **ppObj)
    {
    if (riid == IID_IUnknown)
        {
        *ppObj = static_cast(this) ; 
        AddRef() ;
        return S_OK;
        }

    if (riid == IID_IAdd)
        {
        *ppObj = static_cast(this) ;
        AddRef() ;
        return S_OK;
        }

    //
    //if control reaches here then , let the client know that
    //we do not satisfy the required interface
    //

    *ppObj = NULL ;
    return E_NOINTERFACE ;
    }//QueryInterface method



ULONG   __stdcall CAddObj::AddRef()
    {
    return InterlockedIncrement(&m_nRefCount) ;
    }

    
	
ULONG   __stdcall CAddObj::Release()
    {     
    long nRefCount=0;
    nRefCount=InterlockedDecrement(&m_nRefCount) ;
    if (nRefCount == 0) delete this;
    return nRefCount;
    }

top

Step 6:
We have finished with the functionality part of the Add COM object. As per COM guide lines, every COM object must have a separate implementation of the interface IClassFactory. Clients will use this interface to get an instance of our IAdd interface implementation. The interface IClassFactory, like all other COM interfaces, derives from IUnknown. Therefore we will have to provide an implementation of the IUnknown methods, as well as the IClassFactory methods (LockServer and CreateInstance). Create a new file (name it AddObjFactory.cpp) , declare a class CAddFactory here and make this class derive from IClassFactory.

///////////////////////////////////////////////////////////
//
//AddObjFactory.h
//Contains the C++ class declarations for the IClassFactory implementations
//

class CAddFactory : public IClassFactory
    {

    public:


    //interface IUnknown methods 
    HRESULT __stdcall QueryInterface(
                                REFIID riid , 
                                void **ppObj);
    ULONG   __stdcall AddRef();
    ULONG   __stdcall Release();


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

    private:
    long m_nRefCount;
    };


top

Step 7:
The CAddFactory methods need to be implemented. Create a new file (AddObjFactory.cpp) and provide the method bodies for all the IUnknown and IClassFactory methods. The AddRef, Release and QueryInterface methods have implementations similar to that of class CAddObj. The method CreateInstance is the place, where the class CAddObj is instantiated and and the requested interface pointer is passed back. The method LockServer has not be given any specific implementation.


HRESULT __stdcall CAddFactory::CreateInstance(IUnknown* pUnknownOuter,
                                           const IID& iid,
                                           void** ppv) 
    {
    //
    //This method lets the client manufacture components en masse
    //The class factory provides a mechanism to control the way
    //the component is created. Within the class factory the 
    //author of the component may decide to selectivey enable
    //or disable creation as per license agreements 
    //
    //

    // Cannot aggregate.
    if (pUnknownOuter != NULL)
        {
        return CLASS_E_NOAGGREGATION ;
        }

    //
    // Create an instance of the component.
    //
    CAddObj* pObject = new CAddObj ;
    if (pObject == NULL)
        {
        return E_OUTOFMEMORY ;
        }

    //
    // Get the requested interface.
    //
    return pObject->QueryInterface(iid, ppv) ;
    }


HRESULT __stdcall CAddFactory::LockServer(BOOL bLock) 
    {
    return E_NOTIMPL;
    }

top

Step 8:
An inprocess COM object is nothing more than a simple Win32 DLL, that obeys a certain protocol. Every COM DLL must have an exported function by the name DllGetClassObject. Clients will invoke this function to get an instance of the class factory (either IUnknown or IClassFactory), followed by invocation of the CreateInstance method. Create a new file (Exports.cpp). Implement DllGetClassObject here.

STDAPI DllGetClassObject(const CLSID& clsid,
                         const IID& iid,
                         void** ppv)
    {
    //
    //Check if the requested COM object is implemented in this DLL
    //There can be more than 1 COM object implemented in a DLL
    //

    if (clsid == CLSID_AddObject)
        {
        //
        //iid specifies the requested interface for the factory object
        //The client can request for IUnknown, IClassFactory,
        //IClassFactory2
        //
        CAddFactory *pAddFact = new CAddFactory;
        if (pAddFact == NULL)
            return E_OUTOFMEMORY;
        else
            {
            return pAddFact->QueryInterface(iid , ppv);
            }
        }
    

    //
    //if control reaches here then that implies that the object
    //specified by the user is not implemented in this DLL
    //

    return CLASS_E_CLASSNOTAVAILABLE;
    }

top

Step 9:
Clients need to know when a COM DLL can be unloaded from memory. Deep down,an inprocess COM object gets explicitly loaded by a call to the API LoadLibrary, which brings the specified DLL into the client's process space. An explicitly loaded DLL can be unloaded by a call to FreeLibrary.
COM clients must know when a DLL can be safely unloaded. A client must make sure that there are no instances of any COM object alive , from a particular DLL. To make this accounting simpler , within the COM DLL, we will increment a global variable (g_nComObjsInUse) in the C++ constructors of CAddObj & CAddFactory. Similarly, we will decrement g_nComObjsInUse in their respective destructors.
We will export another COM specified function ,DllCanUnloadNow. The implementation is as follows:

STDAPI DllCanUnloadNow()
    {
    //
    //A DLL is no longer in use when it is not managing any existing objects
    // (the reference count on all of its objects is 0). 
    //We will examine the value of g_nComObjsInUse 
    //

    if (g_nComObjsInUse == 0)
        {
        return S_OK;
        }
    else
        {
        return S_FALSE;
        }

    }

top

Step 10:
The location of a COM object has to be entered into the registry. This can be done through an external .REG file or make the DLL export a function DllRegisterServer. To erase the registry contents, we will export another function DllUnregisterServer. The implementations of these two functions are in the file Registry.cpp . A simple tool like regsvr32.exe can be used to load a specified DLL and then execute DllRegisterServer/DllUnregisterServer.

To make the linker export the 4 functions, we will create a module definition file (Exports.def)

;
;contains the list of functions that are being exported from this DLL
;

DESCRIPTION     "Simple COM object"

EXPORTS
                DllGetClassObject   	PRIVATE
                DllCanUnloadNow     	PRIVATE
                DllRegisterServer   	PRIVATE
                DllUnregisterServer 	PRIVATE

top

Step 11:
We have to give the finishing touches to our AddObj Win32 DLL project. Insert the file IAdd.idl into the project work space.


Set the custom build options for this file.


Insert a command line string in the "Post-build step" dialog box for executing regsvr32.exe at the end of every build.

Build the DLL. Inserting the IDL file into the workspace alleviates the need for external compilation, every time the file is modified. Every time we successfuly build our project, the COM object is registered.

top

Step 12:
To use the AddObj COM object from Visual Basic, creat a simple EXE project and run the following lines of code. Make sure to add a project reference to the IAdd.tlb typelibrary.

    Dim iAdd As CodeGuruMathLib.iAdd
    Set iAdd = CreateObject("CodeGuru.FastAddition")

    iAdd.SetFirstNumber 100
    iAdd.SetSecondNumber 200

    MsgBox "total = " & iAdd.DoTheAddition()

top

Step 13:
The following files were used by us:

IAdd.idl Contains the interface declarations.
AddObj.h Contains the C++ class declaration for the class CAddObj
AddObjFactory.h Contains the C++ class declaration for the class CAddFactory
AddObj.cpp Contains the C++ class implementation for the class CAddObj
AddObj.cpp Contains the C++ class implementation for the class CAddFactory
Exports.cpp Contains the implementations for DllGetClassObject,DllCanUnloadNow & DllMain
Registry.cpp Contains the implementations for DllRegisterServer,DllUnregisterServer
AddObjGuid.h Contains the GUID value for our AddObj COM object.

top

Step 14:
Along with the AddObj.dll, the type library can also be distributed. To simplify the process, the type library IAdd.tlb can also be embedded as a binary resource file in the AddObj DLL . Henceforth, only the DLL file AddObj.dll needs to be distributed to the clients.




top

Step 15:
A Visual C++ client can use use COM interfaces through any of the following:

  • #import "IAdd.tlb" .
  • IAdd.h header file. In this case the DLL vendor must ship the IAdd.h header file along with the DLL.
  • Generate C++ code using some wizard sort of a tool(eg: MFC's Class Wizard)
In Case 1, the compiler creates some intermediate files (.tlh, tli) that contain the expanded interface declarations. Further, the compiler also declares smart interface pointer classes built around the raw interfaces. Smart interface pointer classes make life easier for the COM programmer by properly managing the lifetime of the COM object.
In the following example #import has been done on the file AddObj.dll and not on the IAdd.tlb, because we are shipping the TLB file within the DLL. Otherwise, #import should have been done on the TLB.
Create a new console EXE project in VC++. Type the following contents and compile.

//
///Client.cpp
//
//Demo of client using the COM object from AddObj.dll 
//

#include    <objbase.h>
#include    <stdio.h>
#import     "AddObj.dll"
//
//Here we do a #import on the DLL ,you can also do a #import on the .TLB 
//The #import directive generates two files (.tlh/.tli) in the output folders.
//

void main()
    {

    long n1 =100, n2=200;        
    long nOutPut = 0;

    CoInitialize(NULL);
    CodeGuruMathLib::IAddPtr pFastAddAlgorithm;
    //
    //IAddPtr is not the actual interface IAdd, but a template C++ class (_com_ptr_t)
    //that contains an embedded instance of the raw IAdd pointer
    //While destructing , the destructor makes sure to invoke Release() on the internal
    //raw interface pointer. Further, the operator -> has been overloaded to direct all
    //method invocations to the internal raw interface pointer.
    //
    pFastAddAlgorithm.CreateInstance("CodeGuru.FastAddition");

    pFastAddAlgorithm->SetFirstNumber(n1);//"->" overloading in action
    pFastAddAlgorithm->SetSecondNumber(n2);
    nOutPut = pFastAddAlgorithm->DoTheAddition();

    printf("\nOutput after adding %d & %d is %d\n",n1,n2,nOutPut);
    

    }

top

Downloads

Download source - 114 Kb
Download demo project - 101 Kb


Comments

  • aggregate error

    Posted by Sk Saffiuddin mian on 03/05/2014 10:04pm

    Dear members, I have tried the code to execute but execution shows, abnormal termination error. When I went through the code, its the line pFastAddAlgorithm.CreateInstance("CodeGuru.FastAddition"); QueryInterface() is returning CLASS_E_NOAGGREGATION. Can anyone tell me how to solve this problem.

    Reply
  • I didn't get the output

    Posted by james on 10/03/2013 10:13am

    1: How is the build option set so that every time it registers. 2: I am not able to import the library 3: I don't know where CodeGuruMathLib::IAddPtr is implemented If possible can some convert into visual studio 2008 project.

    Reply
  • step by step com tutorial

    Posted by Sridhar on 06/24/2013 02:11am

    step 11 and 14 are having diagrams, but they are not visible using IE. Update them in this site to make them visible in IE (win 7).

    Reply
  • Thank you

    Posted by Harish on 06/11/2013 02:53am

    where is CLSID_AddObject defined?

    • CLSID_AddObject is defined

      Posted by glm on 07/09/2013 04:30am

      in AddObjGuid.h

      Reply
    Reply
  • thanks

    Posted by Ketan Mayani on 04/23/2013 10:22pm

    Nice one!!!

    Reply
  • Terrible code in a fine example

    Posted by Bilderbikkel on 03/15/2013 10:38am

    Except for the terrible coding style (please, don't use 'void main', don't use globals, do use const), it describes how a COM object works.

    Reply
  • crashes

    Posted by perduta on 12/24/2012 03:07am

    The project files fail to convert so I used the following commands to build it: Midl IAdd.idl cl /LD AddObj.cpp AddObjFactory.cpp Exports.cpp Registry.cpp Exports.def /link Advapi32.lib Ole32.lib User32.lib regsvr32 AddObj.dll cl client.cpp After entering first and second number it stops working and IDK how to debug it. Any suggestions for me to find out why it is crashing?

    Reply
  • Thanx

    Posted by alfredJ on 11/15/2012 08:11am

    Good way of knowing deeply the "complex" world of COM.

    Reply
  • Leaks

    Posted by Krispy on 09/11/2012 07:21pm

    so many memory leaks!!

    • Good it you can elaborate

      Posted by SMS on 12/18/2012 08:46pm

      Krispy, can you please highlight the memory leaks in this sample for the convenience of the community?

      Reply
    Reply
  • Thanks

    Posted by Hitesh on 03/26/2012 08:24am

    It's good description of the COM. Thank you for your help.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

  • With 81% of employees using their phones at work, companies have stopped asking: "Is corporate data leaking from personal devices?" and started asking: "How do we effectively prevent corporate data from leaking from personal devices?" The answer has not been simple. ZixOne raises the bar on BYOD security by not allowing email data to reside on the device. In addition, Zix allows employees to maintain complete control of their personal device, therefore satisfying privacy demands of valued employees and the …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds