- Introduction
- Interfaces
- Step 1:Create the IDL file
- Step 2:Generating the type library
- Step 3:Derive from IAdd
- Step 4:Implement the methods of IAdd
- Step 5:Implementing IUnkown
- Step 6:Factory
- Step 7:Implementing the methods of IClassFactory
- Step 8:Implementing DllGetClassObject
- Step 9:Implementing DllCanUnloadNow
- Step 10:DllRegisterServer-UnregisterServer
- Step 11:Inserting the IDL file into the Workspace
- Step 12:Using the COM object from Visual Basic
- Step 13:Analysis of all the files that were created by us .
- Step 14:Embedding the type library into the ActiveX DLL
- Step 15:Using the COM object from Visual C++ client
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.
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.
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; }
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 |
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 }; ///////////////////////////////////////////////////////////
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; }
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; }
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; };
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; }
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; }
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; } }
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
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.
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()
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. |
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.
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); }