![]() |
Platforms, Frameworks & Libraries »
COM / COM+ »
COM
Beginner
COM from scratch - PART TWOBy Aria AnsariAn article about COM Library. |
VC6, Windows, COM, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
In part one, some background information regarding COM technology was explained, and a simple example showed how a client can use a component's functionality through its interface. In this part, I'll guide the reader to separate the implementation of the component from its client such that the client is no longer bound to the component, and is able to create it through the class factory.
Software component servers provide a way so that functionality can be reused more easily, besides they reduce memory overhead when several applications use the same functionality at the same time, because although each application gets its own copy of the data, they can share the code. By putting a component into a DLL, it's possible to make a kind of component distribution and the DLL becomes the component's server and will contain the implementations of interfaces supported by the component.
In the example, the client and the component were both in the same file, now they should be separated, the client will be in a .exe file, which loads the component into its address space in order to use it, and the component will be served by a DLL. The client should load the DLL into its process and create the component before it can get an interface pointer. If the client links to the CreateInstance() function in the DLL, all other functions of the component are accessible through an interface pointer. So, the solution is just to export the CreateInstance() function from the DLL such that the client can link explicitly to it on the fly. The DLL will be built from the command prompt using one of Microsoft�s command-line tools.
The following figure shows the Server files which are going to be used in making the DLL:

Create a source file (Component.cpp) and put the definition and the implementation of the component's class into it.
Export the CreateInstance() function by adding the following piece of code at the end of the file:

The linker should be informed that the CreateInstance function will be exported, and that can be done by using a module-definition file. A module-definition file is a file with the "def" extension, which contains information about exports, attributes and other information for linking an .EXE file (which has exports) or DLL. In the .def file, the CreateInstance function's export ordinal is chosen to be 1. The following part shows the contents of this file.
;component.def ; Component module-definition file ; LIBRARY Component.dll DESCRIPTION 'Components windows dynamik library' EXPORTS ; Explicit exports can go here CreateInstance @1 PRIVATE
The interface identifier and the interface definition should be known for both the client and the component, and they can be put into two separate files, which are shared between the client and the component. Create another source file (GUID.cpp), which can hold the interface ID:
// // GUID.cpp - Interface ID // #include "objbase.h" extern "C" { extern const IID IID_IComponent = { 0x853b4626, 0x393a, 0x44df, //Data1,Data2,Data3 { 0xb1, 0x3e, 0x64, 0xca, 0xbe, 0x53, 0x5d, 0xbf } }; //Data4 // The extern is required to allocate memory for C++ constants. }
Create a header file (interface.h) with the following content:
// // Interface.h // interface IComponent : IUnknown { virtual void __stdcall Print(const char* msg) = 0 ; } ; // Forward references for GUID extern "C" { extern const IID IID_IComponent ; }
Create a "make" file containing the options for making the DLL with the following content:
#Makefile ################################################################################ # Compiler options: # /c compile without linking # CL cl.exe is a 32-bit tool that controls the Microsoft C # and C++ compilers and linker. # The compilers produce Common Object File Format # (COFF) object (.obj) files. # The linker produces executable (.exe) files # or dynamic-link libraries (DLLs). # ################################## # Linker options: # # /DEF Passes a module-definition (.def) file to the linker # /DEBUG Creates debugging information # /DLL Builds a DLL CPP_FLAGS=/c /MTd /Zi /Od /D_DEBUG EXE_LINK_FLAGS=/DEBUG DLL_LINK_FLAGS=/DLL /DEBUG LIBS=UUID.lib ############################################# # Targets: # CodeProject is just a pseudotarget # CodeProject : component component : Component.dll ######################################### # Shared source files: # GUID.obj : GUID.cpp Cl $(CPP_FLAGS) GUID.cpp ########################################## # Component source files: # Component.obj : Component.cpp Interface.h Cl $(CPP_FLAGS) Component.cpp ######################################## # Link component: # Component.dll : Component.obj GUID.obj Component.def link $(DLL_LINK_FLAGS) Component.obj GUID.obj $(LIBS) /DEF:Component.def
Make the DLL from the Command line by using Microsoft Program Maintenance Utility (NMAKE.EXE). This program is a tool that can build projects based on commands contained in a description file.
The NMAKE utility will create the DLL in the same folder:

The following figure shows the files which are used in making the Client. The Client will be built within the Visual C++ development environment.

Using the AppWizard, create a simple Win32 Console application and choose an empty project.
Create a new source file (Create.cpp) and make a function which takes the DLL's name as parameter, loads the "DLL" and then calls the exported function CreateInstance(). The function's return value would be the return value of the CreateInstance() function, which is an IUnknown interface pointer. In order to link explicitly to the "DLL", the function calls the GetProcAddress function to get the address of the exported function. The GetProcAddress function takes two parameters. The first parameter is a handle to the "DLL" module and the second parameter is the "DLL" name. By calling the LoadLibrary function, it would be possible to obtain the module handle.
// Create.cpp #include "iostream.h" #include "unknwn.h"//IUnknown definition file. #include "Create.h" typedef IUnknown* (*CREATEFUNCPTR)(); ////////////////////////////////////////// IUnknown* CallCreateInstance(char* dllname) { //-----------------------------------------------------------------// // Load dynamic link library into client's process. //Loadlibrary maps a DLL module and return a handle //that can be used in GetProcAddress //to get the address of a DLL function //-----------------------------------------------------------------// HMODULE hm = ::LoadLibrary(dllname); if (hm ==NULL) return NULL; // Get the address of CreateInstance function. CREATEFUNCPTR Function = (CREATEFUNCPTR)::GetProcAddress(hm, "CreateInstance"); if (Function == NULL) return NULL; return Function(); }
Create a new header file (Create.h) with the following contents:
// Create.h IUnknown* CallCreateInstance(char* dllname) ;
Create a new header file (interface.h) with the following contents:
// // Interface.h // interface IComponent : IUnknown { virtual void __stdcall Print(const char* msg) = 0 ; } ; // Forward references for GUID extern "C" { extern const IID IID_IComponent ; }
Create another source file (GUID.cpp), which can hold the interface ID:
// GUID.cpp - Interface ID #include "objbase.h" extern "C" { extern const IID IID_IComponent = { 0x853b4626, 0x393a, 0x44df, //Data1,Data2,Data3 { 0xb1, 0x3e, 0x64, 0xca, 0xbe, 0x53, 0x5d, 0xbf } }; //Data4 // The extern is required to allocate memory for C++ constants. }
Create a source file (Client.cpp) and implement the main function. Call the function made in step 2, in order to instantiate the component and use its methods:
//--------// // Client //--------// int main() { HRESULT hr ; // Get the name of the component to use. char dllname[20]; cout << "Enter the filename of component's server [component.dll]:"; cin >> dllname; ... // calling the CreateInstance function in the // DLL in order to create the component. TRACE("Getting an IUnknown interface pointer...") ; IUnknown* pIUnknown = CallCreateInstance(dllname) ; ... IComponent* pIComponent ; hr = pIUnknown->QueryInterface(IID_IComponent, (void**)&pIComponent); if (SUCCEEDED(hr)) { ... pIComponent->Print("COM from scratch.") ; //using the component's functionality pIComponent->Release() ; ... } ... return 0 ; }
Put the component's server (Component.dll) into the same directory of the client. Now the client is able to load the DLL into its address space and get the address of the CreateInstance function by using LoadLibrary and GetProcAddress functions. Build and run the client program.
The following screen shot shows the client application, after loading the DLL and calling the component's Print method:

Conclusion: Distribution of COM components by servers makes it easy for clients to reuse components' functionality.
One of the advantages of COM components is that it is easy to extend functionalities of an application without rebuilding. As long as an interface is not changed, the client application can still use the component, although its functionality is extended by new changes to its methods. In order to show this advantage of COM components, it's better to view the problem of rebuilding of client applications by a simple example. In the following, a DLL is linked to a client application, you may notice that whenever changes are made to the DLL (for example, by adding a new member variable to a class in the DLL and modifying a member function), the client application will fail to run if it is not rebuild.


//myclass.h class CMyclass { long m_cRef; public: _declspec(dllexport) void Print(const char*msg); };

//client.cpp #include"iostream.h" #include"..\DLL\myclass.h" ///////////////////////////////// void main() { CMyclass classObj; classObj.Print("COM from scratch."); }

CMyclass class and add a new member variable: //myclass.h class CMyclass { long m_cRef; int m_i; // a new member variable public: _declspec(dllexport) void Print(const char* msg); };
Print member function:

Print method has been changed. Since method calls in COM components are indirectly and through their interfaces, there will not be any problem if the methods are modified. Conclusion: COM extends functionalities of applications without rebuilding.
In the example, although the client and the component have been separated, the client is closely related to the component's implementation and should know about the DLL's name, and changing the DLL's name will affect the client. An improvement is that we can move the component from one DLL to another or to other directories. The solution is to replace the CallCreateInstance function with a COM Library function called CoCreateInstance. COM Runtime Library is an integral component of the Windows operating system, which provides the means for clients to locate and instantiate COM objects. COM class objects can be identified by CLSIDs (globally unique identifiers), which are used in order to locate and create an instance of an object. Once the CLSID is obtained, a client application submits the CLSID to the COM run-time library to load the COM object and retrieve an interface pointer. Using the CLSID and the registry, CoCreateInstance locates the specified object, creates an instance of that object, and returns an interface pointer to that object. In order to use CoCreateInstance to create an object, the object must be registered with the system.
The COM Library contains this function. The easiest way of creating a component is by the use of CoCreateInstance function. CoCreateInstance uses a class factory when it creates a component. It takes a CLSID, creates an instance of the corresponding component, and returns an interface for this instance of the component. CoCreateInstance takes 4 in parameters and 1 out parameter (IUnknown*). By passing an IID to CoCreateInstance, the client doesn't need to call QueryInterface on the component after creating it.
CoCreateInstance's parameters:
out parameter is an interface pointer to the created object. Objects that can be created with CoCreateInstance must also be registered with the system. Registration maps a CLSID to the automation component file (.dll or .exe) in which the object resides. If clients will want to obtain CLSIDs at run-time, there must be a way to dynamically locate and load CLSIDs for accessible objects. Furthermore, there has to be some system-wide method for the COM Library to associate a given CLSID (regardless of how the client obtained it) to the server code that implements that class. In other words, the COM Library requires some persistent store of CLSID-to-server mappings that it uses to implement its locator services. The COM implementation on Microsoft Windows uses the Windows system registry as a store for such information. In that registry, there is a root key called "CLSID" under which servers are responsible to create entries that point to their modules. Usually, these entries are created at installation time by the application's setup code, but can be done at run-time if desired. When a server is installed under Windows, the installation program will create a sub-key under "CLSID" for each class the server supports, using the standard string representation of the CLSID as the key name. So the primary entry for a CLSID is a sub-key under CLSID key, which is the CLSID spelled in hex digits within braces. We may also want to associate a CLSID with what is called a programmatic identifier or ProgID, which effectively identifies the same class. A ProgID is a text string without spaces that can be used instead of the CLSID string. The standard ProgID format is <Vendor>.<Component>.<Version>, such as Codeproject.Cmpnt1.1. This format is reasonably unique, and if everyone follows it, there will generally not be a collision. There is also the "VersionIndependentProgID", which has the same format without the version number. Both the ProgID and the VersionIndependentProgID can be registered, with a human-readable name as a value, below the root key. The VersionIndependentProgID is mapped to the ProgID, which is mapped to the CLSID. To create registry entries, you can either write code or create a REG file and simply run it to merge its entries with the registry. The following image shows the registry entries for the Component1 from the Demo Application.

The CoCreateInstance function does not create COM components directly. Instead, it creates a component called class factory, which then creates the desired component. A class factory is a component that creates other components. A particular class factory creates components that correspond only to a single, specific CLSID. The client uses interfaces supported by the class factory for controlling how the class factory creates each component. The standard interface for creating components is IClassFactory interface. IClassFactory like other COM interfaces is derived from IUnknown interface and has two methods:
CreateInstance, which creates an un-initialized object of a specified CLSID.
LockServer, which locks the object's server in memory, allowing new objects to be created more quickly. In the following, a class factory is defined in order to create the COM component in the example:
/////////////////////////////////////////////////////////// // // 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() ; // IClassFactory virtual HRESULT __stdcall CreateInstance(IUnknown* pUnkOuter, 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,LPVOID* 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* pUnkOuter, const IID& iid,void** ppv) { HRESULT hr; if (pUnkOuter != NULL) { return CLASS_E_NOAGGREGATION ; } CComponent* pComponent = new CComponent ; if (pComponent == NULL) { return E_OUTOFMEMORY ; } // Get the requested interface. hr = pComponent->QueryInterface(iid,(void**) ppv) ; if(FAILED(hr)) pComponent->Release() ; return hr ; } //-----------------------------------------------------------------------// // LockServer // Called by the client of a class object to keep a server open in memory, // allowing instances to be created more quickly. //-----------------------------------------------------------------------// /////////////////////////////////////////////////// HRESULT __stdcall CFactory::LockServer(BOOL bLock) { return S_OK ; }
Before going to more details, it's better to have an overview about creation of the component via COM Library:
CoCreateInstance, which is implemented in COM Library.
CoCreateInstance is implemented using CoGetClassObject function.
CoGetClassObject calls DllGetClassObject, which is implemented in DLL server and its job is to create the class factory for the component.
DllGetClassObject queries the class factory for IClassFactory interface, which is returned to CoCreateInstance function.
CoCreateInstance uses IClassFactory interface to call its CreateInstance method.
IClassFactory::CreateInstance(...) uses the new operator to create the component and it queries the component for its interface.
CoCreateInstance releases the class factory and returns an interface pointer to the client.
Print method of the component and uses its functionality. The following image illustrates these steps:

So, in order to improve the example, we need to:
CFactory methods.
DllGetClassObject in the component server or the DLL instead of CreateInstance function.
It's also easier to make the DLL within the Visual C++ development environment. In the following, these steps will be implemented:


// Component.cpp : Defines the initialization routines for the DLL. // #include "stdafx.h" #include <afxdllx.h> #include "interface.h" #include <objbase.h> #include "iostream.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif // // Component.cpp ///////////////////////////////////////////// BOOL APIENTRY DllMain(HINSTANCE InsModule, DWORD dwReason, void* lpReserved) { return TRUE; }
CreateInstance function, because the class factory is going to create the component. The DllGetClassObject function will be called from within the CoGetClassObject function, when the class context is a DLL, and as mentioned before, its job is to create the class factory for the component. Implement this function in the Component.cpp file:
///////////////////////////////////////////////////////////////////////// STDAPI DllGetClassObject(const CLSID& clsid, const IID& iid, void** ppv) { if (clsid != CLSID_Component) return CLASS_E_CLASSNOTAVAILABLE; // Create class factory. CFactory* pFactory = new CFactory ; if (pFactory == NULL) return E_OUTOFMEMORY; // Get requested interface. HRESULT hr = pFactory->QueryInterface(iid, ppv); pFactory->Release(); return hr; }
Compile and build the DLL (Component.dll).
Using the GUIDGEN.EXE, create a CLSID for the Component's class:
{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}
static const GUID CLSID_Component =
{ 0x49bf12f1, 0x5041, 0x48da,
{ 0x9b, 0x44, 0xaa, 0x2f, 0xaa, 0x63, 0xae, 0xfb } };
Create a file with ".reg" extension (component.reg) in order to create registry entries for the component (using the CLSID):
REGEDIT HKEY_CLASSES_ROOT\Codeproject.Component.1 = Codeproject Component Version 1.0 HKEY_CLASSES_ROOT\Codeproject.Component.1\CLSID = {49BF12F1-5041-48da-9B44-AA2FAA63AEFB} HKEY_CLASSES_ROOT\Codeproject.Component = Codeproject Component HKEY_CLASSES_ROOT\Codeproject.Component\CurVer = Codeproject.Component.1 HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB} = Codeproject Component 1.0 HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}\InprocServer32 = c:\codeproject\component.dll HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}\ProgID = Codeproject.Component.1 HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}\ VersionIndependentProgID = Codeproject.Component
Activate the registry file by clicking on that. After running the registry file, the entries will be stored in Windows registry system. The following image shows these entries:

That's it; the chain now is totally broken to pieces. Check it out with the client.
Now, although the component's server (component.dll) resides in the directory C:\codeproject, the client can easily load it and use its functionality through the class factory and COM Library, and this is the way COM components usually are created and used by their clients. The following shows how the client uses the component through the COM Library:
//-----------// // Client //-----------// void main() { HRESULT hr; IUnknown* pIUnknown; IComponent* pIComponent; IClassFactory* pIClassFactory; ::CoInitialize(NULL); /* //Once the CoCreateInstance is called, the component //will be created and the client can not //control it, that's why CoCreateInstance is inflexible //and the solution is to call CoGetClassObject function hr = ::CoCreateInstance(CLSID_Component,NULL, CLSCTX_INPROC_SERVER,IID_IUnknown,(void**)&pIUnknown) ; if (SUCCEEDED(hr)) { hr=pIUnknown->QueryInterface(IID_IComponent,(void**)&pIComponent); if(SUCCEEDED(hr)) pIComponent->Print("COM from scratch."); } */ //-------------------------------// // improvement of the client code //------------------------------// // By calling the CoGetClassObject function, the client can control // creation of the component hr=CoGetClassObject(CLSID_Component,CLSCTX_INPROC_SERVER, NULL,IID_IClassFactory,(void**)&pIClassFactory); if (SUCCEEDED(hr)) { hr=pIClassFactory->CreateInstance(NULL, IID_IComponent,(void**)&pIComponent); if(SUCCEEDED(hr)) pIComponent->Print("COM from scratch."); } ::CoUninitialize (); }
Part three is explained in the next article.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 17 Apr 2004 Editor: Smitha Vijayan |
Copyright 2004 by Aria Ansari Everything else Copyright © CodeProject, 1999-2009 Web21 | Advertise on the Code Project |