Page #53 (From C to COMA Quick Tour)

< BACK  NEXT >
[oR]

Revisiting C++ Code

Multiple Classes in a Server

The DLL that we created is a housing for the implementation of class CVcr a class that implements the interfaces associated with CLSID_VCR. Such a DLL is referred to as a COM server. If you think about it, there is no reason why a COM server cannot house more than one class. In fact, in many cases it makes sense to house a number of related classes together.

COM supports this notion of a server supporting multiple classes.

If we support multiple classes in one server, the question that then arises is, which instance of the class should DllGetClassObject return? This is where the first parameter to DLLGetClassObject makes sense. Depending on the specified CLSID, the implementation knows which instance to return.

At this point, it would be a good idea to recap some of the terminology used under COM:

  • An interface is an abstract definition for communication between a client and a server. In our case, we have two such interfaces IVideo and ISVideo.

  • An implementation is a concrete data type that supports one or more interfaces by providing program code for each of the interface methods. In our case, it is the C++ class, CVcr.

  • A COM object, or just an object, is an instance of the implementation. In our case, it would be new CVcr.

  • A software vendor who develops a COM server can publish the interfaces it supports by way of defining one or more coclasses in the IDL file. A coclass (or a COM class) is a named declaration that represents concrete instantiable type and the potential list of interfaces it exposes. In our case, it is the IDL declaration coclass VCR. All that is known about a coclass is its CLSID and the potential list of interfaces it supports.

Multiple Instances of a Class

Consider the following scenario.

Our TV customers are demanding picture-in-picture capability. With this ability, the TV can display information from two different sources at the same time. In our case, it essentially boils down to the client being able to create two instances of the VCR class.

Cool! You say. All we need to do is to call CoGetClassObject two times, one for each source, as shown below:

 ISVideo* pSVideoSource1 = NULL;  HRESULT hr = ::CoGetClassObject(CLSID_VCR, CLSCTX_ALL, NULL,    IID_ISVideo, reinterpret_cast<void**>(&pSVideoSource1));  ISVideo* pSVideoSource2 = NULL;  hr = ::CoGetClassObject(CLSID_VCR, CLSCTX_ALL, NULL, IID_ISVideo,    reinterpret_cast<void**>(&pSVideoSource2)); 

You are thinking in the right direction. However, recall what goes on when CoGetClassObject is called. First, LoadLibrary is called, followed by GetProcAddress, which gets the memory address of the DllGetClassObject function. Finally, function DllGetClassObject is invoked which in turn searches for the specified CLSID.

Look at the problem we are trying to solve we just want multiple instances of the same class to be created. For efficiency, we do not want LoadLibrary, GetProcessAddress, and search-forCLSID to be called multiple times.

If CoGetClassObject would return an object (specific to the requested CLSID) that supports a mechanism to create instances of the class, our problem is solved. We could just call CoGetClassObject once and cache the returned object. Each time a new instance is desired, we can ask the cached object (via some interface) to create a new instance of the class.

This intermediate object that lets you create instances of the actual implementation class is called the class object.

Class Object

A class object implements the instance-less functionality of the specified CLSID. As I mentioned earlier, it is desired that this object be cached and reused by COM. Therefore, it does not make sense to have multiple instances of this object. Therefore, COM mandates that a class object be a singleton, that is, it can have just one instance.

Even though a class object is technically a COM object , it is a special kind of object. It should be viewed as a vehicle to get to the more meaningful object the one that implements the interfaces you wish to use.

If you recall the earlier implementation of DllGetClassObject, the class object and our COM object were the same. This is not considered illegal. What is illegal is that we are creating a new instance of the class object each time DllGetClassObject is called, thus violating the singleton rule.

How can you ensure that DllGetClassObject returns the same object each time it is called? I know what you are thinking just define a static variable to store the class object as shown here:

 STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid,    LPVOID* ppv)  {   if (!IsEqualIID(rclsid, CLSID_VCR)) {     return CLASS_E_CLASSNOTAVAILABLE;    }    static CVcr* g_pVCR = NULL;    if (NULL == g_pVCR) {     g_pVCR = new CVcr;      if (NULL == g_pVCR) {       return E_OUTOFMEMORY;      }      g_pVCR->AddRef();      // Keep the pointer alive for                             // server lifetime    }    HRESULT hr = g_pVCR->QueryInterface(riid, ppv);    return hr;  } 

With this logic, the class object is allocated just once the first time DllGetClassObject is called.

graphics/01icon01.gif

The above code makes an extra call to AddRef. The class object has to stay alive during the lifetime of the server. If the reference count were not incremented artificially, the object would get deleted after the clients have released references to the object. In that case, the next call to DllGetClassObject will return a pointer to an invalid memory location.


Now let s turn our attention to the client code. Our TV code makes two calls to CoGetClassObject in order to support picture-in-picture. However, there is a problem here as the objects being returned are the same, both the displayed images will be identical. It is quite likely that you would like to see two different images. After all, you paid extra bucks to get the picture-in-picture feature.

The problem is, in writing our server code, we treated the class object and the implementation object as one and the same. Not only should we separate the two, but we should also provide a mechanism on the class object to return a new implementation object. This way, the TV client can get the class object (just once) and request it to create two new instances (or as many as it wants) of the SVideo source.

Class Factories

Obviously, the class object has to support a new interface that would let the client create new instances of the implementation class. COM SDK defines a standard interface called IClassFactory that a class object can support. Following is its definition taken from SDK-supplied file unknwn.idl. For clarity, I have removed some unneeded information.

 [   object,    uuid(00000001-0000-0000-C000-000000000046),    pointer_default(unique)  ]  interface IClassFactory : IUnknown  {   HRESULT CreateInstance(     [in, unique] IUnknown * pUnkOuter,      [in] REFIID riid,      [out, iid_is(riid)] void **ppvObject);    HRESULT LockServer([in] BOOL fLock);  }; 

The method that is of interest is CreateInstance. It has three parameters:

  • The first parameter is used for something called aggregation. We will talk about aggregation in a later section. For now, we will specify NULL as its value to indicate that we are not interested in aggregation. [1]

    [1] Recall from Chapter 2 that the [unique] attribute on a method parameter makes it possible to specify NULL as a valid value.

  • The second parameter is the IID of the interface. In our case, this is IID_ISVideo.

  • The third parameter is the return value; it returns a pointer to the newly created instance.

Let s implement our class object logic.

Following is the C++ class definition of the class object:

 class CVcrClassObject : public IClassFactory  { public:    CVcrClassObject();    ~CVcrClassObject();    // IUnknown interface    STDMETHOD(QueryInterface)(REFIID iid, void** pp);    STDMETHOD_(ULONG, AddRef)();    STDMETHOD_(ULONG, Release)();    // IClassFactory interface    STDMETHOD(CreateInstance)(IUnknown* pUnkOuter, REFIID riid,      void** ppV);  STDMETHOD(LockServer)(BOOL fLock);  private:    long m_lRefCount;  }; 

The class has to support two interfaces the first is the ubiquitous interface IUnknown (we know we always have to support this), and the second one is IClassFactory.

Implementing IUnknown basically amounts to using boilerplate code. You can cut and paste code from the CVcr implementation. Just remember to change the QueryInterface code to support IUnknown and IClassFactory. That code is not shown here. You can find it on the CD.

Following is the implementation for the IClassFactory method CreateInstance:

 STDMETHODIMP CVcrClassObject::CreateInstance(   IUnknown* pUnkOuter,    REFIID riid,    void** ppV)  {   *ppV = NULL; // always initialize the return value    if (NULL != pUnkOuter) {     // we don't support aggregation      return CLASS_E_NOAGGREGATION;    }    CVcr* pVCR = new CVcr;    if (NULL == pVCR) {     return E_OUTOFMEMORY;    }    HRESULT hr = pVCR->QueryInterface(riid, ppV);    if (FAILED(hr)) {     delete pVCR;    }    return hr;  } 

The second IClassFactory method, LockServer, is currently of no interest to us. We just return a standard HRESULT error code, E_NOTIMPL, to indicate that the logic has not been implemented for the method.

 STDMETHODIMP CVcrClassObject::LockServer(BOOL fLock)  {   return E_NOTIMPL;  } 

We now need to modify the DllGetClassObject code to use CVcrClassObject.

 STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid,    LPVOID* ppv)  {   if (!IsEqualIID(rclsid, CLSID_VCR)) {     return CLASS_E_CLASSNOTAVAILABLE;    }    static CVcrClassObject* g_pVCRClassObject = NULL;    if (NULL == g_pVCRClassObject) {     g_pVCRClassObject = new CVcrClassObject;      if (NULL == g_pVCRClassObject) {       return E_OUTOFMEMORY;      }      // Keep the pointer alive for server lifetime      g_pVCRClassObject->AddRef();    }    HRESULT hr = g_pVCRClassObject->QueryInterface(riid, ppv);    return hr;  } 

This completes the changes required for the server code.

Now let s modify the TV client code to create new instances from the class object. Following is the revised code. For brevity, I have removed error-checking logic.

 int main(int argc, char* argv[])  {   ::CoInitialize(NULL);    IClassFactory* pCF = NULL;    ::CoGetClassObject(CLSID_VCR, CLSCTX_ALL, NULL,      IID_IClassFactory, reinterpret_cast<void**>(&pCF));    ISVideo* pSVideoSource1 = NULL;    pCF->CreateInstance(NULL, IID_ISVideo,      reinterpret_cast<void**>(&pSVideoSource1));    ISVideo* pSVideoSource2 = NULL;    pCF->CreateInstance(NULL, IID_ISVideo,      reinterpret_cast<void**>(&pSVideoSource2));    // Done with class factory    pCF->Release(); pCF = NULL;    // For now, assume the following two calls are    // executing simultaneously    UseSVideo(pSVideoSource1);    UseSVideo(pSVideoSource2); // Picture-in-picture    pSVideoSource1->Release();    pSVideoSource2->Release();    ::CoUninitialize();    return 0;  } 

We are done defining the important concepts and implementing the sample code. At this point, I would like you to recall the meaning of the following terms: Interface, COM class (coclass), C++ implementation class, COM object, class object, and class factory. If you feel you are not clear on any of these terms, please read the previous section once again. These terms are fundamental to understanding the COM programming model (not to mention the rest of the book).

Let s look at some of the optimizations we can do.

Optimizations

If you examine the client code, it is obvious that a class object itself is not that meaningful; it is just a way of getting to a more meaningful object the object implementing the desired interface. Therefore, it makes sense to combine the capabilities of CoCreateClassObject and CreateInstance into one function, thereby saving one roundtrip to the server. The SDK provides an API, CoCreateInstance, to do just this:

 STDAPI CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter,    DWORD dwClsContext, REFIID riid, LPVOID* ppv); 

Using this API, the TV code can be written as:

 int main(int argc, char* argv[])  {   ::CoInitialize(NULL);    ISVideo* pSVideoSource1 = NULL;    ::CoCreateInstance(CLSID_VCR, NULL, CLSCTX_ALL, IID_ISVideo,      reinterpret_cast<void**>(&pSVideoSource1));    ISVideo* pSVideoSource2 = NULL;    ::CoCreateInstance(CLSID_VCR, NULL, CLSCTX_ALL, IID_ISVideo,      reinterpret_cast<void**>(&pSVideoSource2));    // For now, assume the following two calls are    // executing simultaneously    UseSVideo(pSVideoSource1);    UseSVideo(pSVideoSource2); // Picture-in-picture    pSVideoSource1->Release();    pSVideoSource2->Release();    ::CoUninitialize();    return 0;  } 

Let s recap what we have achieved so far in this section.

We realized that a class object should really be a singleton. However, in order to be able to create newer COM objects (instances of the implementation class), the class object should support the IClassFactory interface. The client can then call CoGetClassObject followed by CreateInstance, or can combine them into just one operation, CoCreateInstance.

Note that COM does not mandate that a class object support the IClassFactory interface. COM monikers, for example, require a different interface on the class objects. [2] However, if interface IClassFactory is not supported on the class object, CoCreateInstance will fail with error code REGDB_E_CLASSNOTREG (0x80040154).

[2] Covering COM monikers is outside the scope of this book.

Two problems still need to be addressed. The first one has to do with the way we chose to publish our class information, and the second one deals with memory consumption and leakage.

Storing Configuration Information

If you recall, the way we registered our server code was by adding an entry in the Windows registry specifying the path name of the server. There are two problems with this approach:

  • Each time the DLL is moved to a different directory, one has to edit the registry entry manually and type in the new path name. Not only is this a pain in the neck, it is also error-prone.

  • If the DLL is removed from the machine, one has to remember to delete the stale registry entries; that is, manually unregister the class.

Windows provides APIs to obtain the path name of a running executable as well as to add/delete registry entries. If this registration process is pushed to the COM server, the server itself can then handle registering/unregistering programmatically. [3]

[3] Registering a DLL file path using explicit code is counter-intuitive to the main theme that permeates through COM+ that of using out-of-band attributes instead of in-band explicit code. However, in the spirit of easy migration of legacy COM code to COM+, COM+ 1.0 made an exception for the file path as well as a few other attributes.

COM expects a COM server [4] to implement two functions to handle self-registration.

[4] The COM servers we refer to are all DLL-based, which is what COM+ promotes. In the era of classic COM, a server could be a stand-alone executable.

 STDAPI DllRegisterServer(void);    // register yourself  STDAPI DllUnregisterServer(void);  // unregister yourself 

Let s implement the first function. This requires obtaining the path name of the DLL programmatically.

Under Windows, a DLL loaded into a process space can obtain its own path name by calling a Win32 API GetModuleFileName. Following is its prototype:

 DWORD GetModuleFileName(HINSTANCE hInst, LPTSTR lpFilename,    DWORD nSize ); 

Each loaded DLL in the process space is assigned a value, called a module handle, which is unique within the process. By specifying the module handle as the first parameter, the DLL path name can be obtained programmatically.

In order to call this function from a DLL, the DLL needs to know its own module handle. How can the DLL get its own module handle?

A DLL can implement an optional function called DllMain. [5] The operating system calls this function and passes the module handle as a parameter when the DLL is loaded for the first time within a process. This gives the DLL a chance to copy the module handle in a global location so that the value can be accessed from other DLL functions, as shown here:

[5] A DLL has to have DllMain function. However, if this function is not implemented explicitly, the linker uses the default implementation (that just returns S_OK) provided in the standard run-time library.

 static HINSTANCE g_hInstance = NULL;  // DLL Entry Point  BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason,    LPVOID /*lpReserved*/)  {   if (dwReason == DLL_PROCESS_ATTACH)    {     g_hInstance = hInstance;    }    return TRUE;                // ok  } 

Here is the code for registering the DLL. The registry manipulation APIs such as RegCreateKey and RegSetValueEx are documented in the Win32 SDK.

 HRESULT AddRegistryEntry(LPCTSTR pszSubKey, LPCTSTR pszValueName,    LPCTSTR pszValue)  {   HKEY hSubKey = NULL;    LONG lRetVal =      ::RegCreateKey(HKEY_CLASSES_ROOT, pszSubKey, &hSubKey);    if (ERROR_SUCCESS != lRetVal) {     return HRESULT_FROM_WIN32(lRetVal);    }    int len= lstrlen(pszValue) + 1; //include terminating NULL char    lRetVal = ::RegSetValueEx(hSubKey, pszValueName, 0, REG_SZ,      reinterpret_cast<const BYTE*>(pszValue), len);    ::RegCloseKey(hSubKey);    return HRESULT_FROM_WIN32(lRetVal);  }  // DllRegisterServer - Adds entries to the system registry  STDAPI DllRegisterServer(void)  {   TCHAR szPath[MAX_PATH];    ::GetModuleFileName(g_hInstance, szPath,      sizeof(szPath)/sizeof(TCHAR));    HRESULT hr = AddRegistryEntry( "CLSID\\{318B4AD3-06A7-11d3-9B58-0080C8E11F14}\\InprocServer32",      "", szPath);    return hr;  } 

The registry entry that DllRegisterServer adds is the same as the one we previously added using rededit.exe.

Unregistering a server is even easier we just need to delete the registry key CLSID\{318B4AD3-06A7-11d3-9B58-0080C8E11F14} and the subkeys underneath.

graphics/01icon01.gif

The API to delete a Windows registry key, RegDeleteKey, expects that the key (or the subkey) being deleted has no child subkeys. Therefore, one has to recursively delete the child subkeys first.


Here is the final code for unregistering the DLL. Given that we do not have any subkeys to delete, the code is straightforward.

 // DllUnregisterServer - Removes entries from the system registry  STDAPI DllUnregisterServer(void)  {   long lRetVal = ::RegDeleteKey(HKEY_CLASSES_ROOT,  "CLSID\\{318B4AD3-06A7-11d3-9B58-0080C8E11F14}\\InprocServer32");    if (ERROR_SUCCESS != lRetVal) {     return HRESULT_FROM_WIN32(lRetVal);    }    lRetVal = ::RegDeleteKey(HKEY_CLASSES_ROOT,      "CLSID\\{318B4AD3-06A7-11d3-9B58-0080C8E11F14}");    return HRESULT_FROM_WIN32(lRetVal);  } 

We now need to export the DllRegisterServer and DllUnregisterServer functions, similar to what we did for DllGetClassObject. Following is the revised module-definition file:

 ; Module-Definition File Vcr.Def  LIBRARY   "MyVcr.DLL"  EXPORTS    DllGetClassObject       PRIVATE    DllRegisterServer       PRIVATE    DllUnregisterServer     PRIVATE 

The OS includes a program called regsvr32.exe to deal with DLL registration. To register a DLL, supply the DLL path name as the argument:

 regsvr32 MyVcr.dll 

To unregister a DLL, specify -u as a switch:

 regsvr32  u MyVcr.dll 

One can easily guess that regsvr32.exe just loads the specified DLL and invokes one of the two registration functions. Implementing such a program is left as an exercise for you.

Memory Cleanup

Let s reexamine our class object creation code:

 static CVcrClassObject* g_pVCRClassObject = NULL;  g_pVCRClassObject = new CVcrClassObject;  if (NULL == g_pVCRClassObject) {   return E_OUTOFMEMORY;  }  g_pVCRClassObject->AddRef(); 

Recall that we bumped up the reference count to ensure that the class object does not get deleted during the lifetime of the server. However, when the server is no longer in use, it is our responsibility to release the class object. Otherwise, we will have a memory leakage.

How does COM determine that a server is no longer in use? I will answer this in the next section. For now, the important thing to note is, when the server is no longer in use, COM unloads the DLL.

Recall that function DllMain was being called when the DLL gets loaded. This same function is also called when the DLL is getting unloaded, giving us the perfect opportunity to release the class object. Following is the code:

 static CVcrClassObject* g_pVCRClassObject = NULL;  // DLL Entry Point  extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance,    DWORD dwReason, LPVOID /*lpReserved*/)  {   if (DLL_PROCESS_ATTACH == dwReason) {     g_hInstance = hInstance;    }else    if (DLL_PROCESS_DETACH == dwReason) {     if (NULL != g_pVCRClassObject) {       g_pVCRClassObject->Release();      }    }    return TRUE;  // ok  } 

Note that I had to move the static variable g_pVCRClassObject to the file level scope, as two different functions, DllGetClassObject and DllMain, need to use the same variable.

As much as you dislike using global variables, they are an unavoidable evil sometimes. However, the list of global variables seems to be increasing in our case. First, we had one global variable g_hInstance. Now we ve added g_pVCRClassObject.

One improvement we can do is to group all global parameters under one structure (or class) and have just one global variable for the whole structure, as shown here:

 // File MyModule.h  #pragma once               // Visual C++ optimization to avoid loading                             // a header file multiple times  class CVcrClassObject;     // forward declaration  struct MYMODULEINFO  {   MYMODULEINFO()    {     hInstance = NULL;      pVCRClassObject = NULL;    }    HINSTANCE hInstance;    CVcrClassObject* pVCRClassObject;  };  extern MYMODULEINFO g_MyModule; 

While we are in this good programming practices mode, I would like to introduce the notion of one standard include file. The idea is that there are many project-specific include files that get used frequently but change infrequently. All such header files can be referenced through just one header file. Any other source module in the project would then need to include this one header file, besides any other header file it may need. Using such a technique has a couple of advantages:

  • Any macro that has to be defined project-wide can be defined at one place.

  • Microsoft s compiler can precompile this header just once, thereby reducing project build time.

If you have been programming in Microsoft s world, you would know that such a file is typically named StdAfx.h. Here is our version of this file:

 // File StdAfx.h  #pragma once  #define WIN32_LEAN_AND_MEAN  #include <windows.h>  #include <ole2.h>  #include "MyModule.h" 

File windows.h is needed for using any Win32 API and ole2.h is needed for using any COM API. Until now, they were getting referenced automatically through the MIDL-generated file Video.h. Here, we reference them explicitly.

graphics/01icon02.gif

Macro WIN32_LEAN_AND_MEAN can be defined to exclude rarely used header files referenced in windows.h. This speeds up compilation time.


When to Unload a DLL?

The problem of releasing the class object is fixed. However, in this process, we overlooked an important aspect. Earlier, I conveniently sneaked in the text that when the server is no longer is use, the COM library unloads the DLL. But how does the COM library know a server is not in use?

The COM library mediated the process of delivering a class object from the client to the server. However, it does not know how many COM objects the client created, for example, by calling the IClassFactory::CreateInstance method. As long as the client has a reference to even one object, the server has to stay loaded. Therefore, the COM library cannot just arbitrarily unload the server.

The COM library may not know the number of server objects that have outstanding references, but the server sure does. After all, it is the server that is issuing objects to the client. Therefore, it makes sense that the COM library asks the server to see if it is fine to unload.

The COM specifications define that a server implement a function called DllCanUnloadNow. The function should return S_OK code if it is okay to unload the DLL. Otherwise, the return code should be S_FALSE.

 STDAPI DllCanUnloadNow(void)  {   return (AnyOutstandingReference()) ? S_OK : S_FALSE;  } 

To keep track of the outstanding references, a typical technique is to use a global counter (yet another global variable), as shown here:

 struct MYMODULEINFO  {   MYMODULEINFO()    {     hInstance = NULL;      pVCRClassObject = NULL;      lCount = 0;    }    HINSTANCE hInstance;    CVcrClassObject* pVCRClassObject;    ULONG lCount;  }; 

Using this counter, function DllCanUnloadNow can be implemented as:

 STDAPI DllCanUnloadNow(void)  {   return (g_MyModule.lCount==0) ? S_OK : S_FALSE;  } 

The variable needs to be incremented each time an object is created and decremented each time an object is deleted. What better place than the object constructor and destructor to add this logic?

 CVcr:: CVcr()  {   ...    g_MyModule.lCount++;  }  CVcr::~CVcr()  {   ...    g_MyModule.lCount ;  } 

We also need to add a similar logic to the class object code. After all, a class object is a COM object. It can have outstanding references as well. However, keeping track of outstanding references for a class object is a little tricky. The problem is, the server will always keep the class object alive by bumping up the reference count to one initially. If the count logic is added in the constructor, as we did for class CVcr, the global object count will never become zero. Hence, a call to DllCanUnloadNow will always return S_FALSE, and the server will never be unloaded.

For a cached object such as the class object, we know that the object reference count will be one if there are no clients holding the object. Otherwise, the reference count will be two or more. For such an object, we can move our global object count logic into the reference count code, that is, within the implementation of the AddRef and Release methods, as shown below:

 STDMETHODIMP_(ULONG) CVcrClassObject::AddRef()  {   long lRetVal = (++m_lRefCount);    if (lRetVal == 2) {     // a client is requesting for a AddRef      g_MyModule.lCount++;    }    return lRetVal;  }  STDMETHODIMP_(ULONG) CVcrClassObject::Release()  {   ULONG lRetVal = ( m_lRefCount);    if (1 == lRetVal) {     // all clients have released the references      g_MyModule.lCount ;    }else    if (0 == lRetVal) {     delete this;    }    return lRetVal;  } 

There is still one more problem that we need to address. A client releases an object by calling the method Release. This call goes directly to the server, not through the COM library. Therefore, when a client releases the last object it is holding from a server, the COM library still is not aware that the server can potentially be freed.

The COM SDK defines an API, CoFreeUnusedLibraries, that the clients can call at strategic locations in their code to unload any COM servers that are no longer in use. The COM library keeps a list of each DLL server that has been loaded, either directly (using CoGetClassObject) or indi-rectly (using CoCreateInstance). When CoFreeUnUsedLibraries is called, it walks through the list and unloads a DLL, if it is okay to do so. Following is the pseudo-logic for the implementation:

 for each DLL-handle in the list  {   pFnCanUnloadNow = GetProcAddress(handle, "DllCanUnloadNow");    HRESULT hr = (*pfnCanUnloadNow)();    if (S_OK == hr) {     FreeLibrary(handle);      Remove handle from the list.    }  } 

graphics/01icon02.gif

Call CoFreeUnusedLibraries at some strategic places in your client code. This causes the unused COM DLLs to be unloaded from the address space of the process, thus reducing some memory footprint.


Optimizations

Calling CoFreeUnusedLibraries unloads all the servers that are currently not in use. Note the emphasis on currently. There are cases when a client releases all the objects from a server but may need to instantiate objects once again from the same server at a later time. This will result in loading the server (the DLL) once again.

Loading and unloading a DLL can get expensive in terms of performance. What is desired is the ability to prevent a specific server from getting unloaded even if the server is not currently in use.

It turns out that the COM designers had already thought of this problem when designing the IClassFactory interface.

Recall that this interface had a method called LockServer that we never really implemented. The code is shown here:

 STDMETHODIMP CVcrClassObject::LockServer(BOOL fLock)  {   return E_NOTIMPL;  } 

A client can call this method to keep a server from getting unloaded. Parameter fLock, if TRUE, indicates that the server be kept in memory. A FALSE value indicates that it is okay to unload it.

The implementation can be tied very well with our global counter variable. So far, we were only using it for keeping the object count. However, there is no reason not to use the same global variable for the LockServer method. The following is such an implementation:

 STDMETHODIMP CVcrClassObject::LockServer(BOOL fLock)  {   if (TRUE == fLock) {     g_MyModule.lCount++;    }else {     g_MyModule.lCount ;    }    return S_OK;  } 

It is the client s responsibility, of course, to call LockServer(TRUE) and LockServer(FALSE) in a pair. Otherwise, the DLL will always stay in memory.

Congratulations! You just finished writing a complete DLL-based COM server that adheres to all the COM rules.

Now let s reorganize the implementation code a little to make it more manageable.

Code Cleanup

If you recall, our global variable g_MyModule has three data members that are publicly accessible. The structure is shown here for your review:

 struct MYMODULEINFO  {   MYMODULEINFO()    {     hInstance = NULL;      pVCRClassObject = NULL;      lCount = 0;    }    HINSTANCE hInstance;    CVcrClassObject* pVCRClassObject;    ULONG lCount;  }; 

The first thing I would like to do is mark these data members as private. And while I am at it, I would also like to change the structure definition to a class definition, as shown here:

 // File MyModule.h  #pragma once  class CVcrClassObject; // forward declaration  class CMyModule  { public:    CMyModule();    ~CMyModule();    ...  private:    HINSTANCE m_hInstance;    CVcrClassObject* m_pVCRClassObject;    LONG m_lCount;  };  extern CMyModule g_MyModule;  // File MyModule.cpp  CMyModule::CMyModule()  {   m_hInstance = NULL;    m_pVCRClassObject = NULL;    m_lCount = 0;  }  CMyModule::~CMyModule()  {   _ASSERT (NULL == m_pVCRClassObject);    _ASSERT (0 == m_lCount);  } 

Data member m_hInstance needs to be initialized once the execution begins, but it should be accessible anytime during the execution:

 HRESULT CMyModule::Init(HINSTANCE hInst)  {   m_hInstance = hInst;    return S_OK;  }  HINSTANCE CMyModule::GetModuleInstance()  {   return m_hInstance;  } 

The idea behind defining method Init is to isolate all the initialization logic to just one method.

Data member m_lCount is used as a global counter:

 LONG CMyModule::Lock()  {   return ++m_lCount;  }  LONG CMyModule::Unlock()  {   return  m_lCount;  }  LONG CMyModule:: GetLockCount ()  {   return m_lCount;  } 

The final data member m_pVCRClassObject is used to create a class object. I will define a new method, CMyModule::GetClassObject, and move the logic from DllGetClassObject to this method.

 HRESULT CMyModule::GetClassObject(REFCLSID rclsid, REFIID riid,    LPVOID* ppv)  {   if (!IsEqualIID(rclsid, CLSID_VCR)) {     return CLASS_E_CLASSNOTAVAILABLE;    }    if (NULL == m_pVCRClassObject) {     m_pVCRClassObject = new CVcrClassObject;      if (NULL == m_pVCRClassObject) {       return E_OUTOFMEMORY;      }      // Keep the pointer alive for server lifetime      m_pVCRClassObject->AddRef();    }    return m_pVCRClassObject->QueryInterface(riid, ppv);  } 

The extra reference on the class object has to be released on execution termination. I will define another method, CMyModule::Term, and move the logic from DllMain to this method:

 void CMyModule::Term()  {   if (NULL != m_pVCRClassObject) {     m_pVCRClassObject->Release();      m_pVCRClassObject = NULL;    }    _ASSERT (0 == m_lCount);  } 

Similar to the method Init, which isolated all the initialization logic to just one method, the method Term can be used to isolate all the termination logic.

Finally, I would like to move all the registration logic from DllRegisterServer and DllUnregisterServer to class CMyModule.

 // RegisterServer - Adds entries to the system registry  HRESULT CMyModule::RegisterServer(void)  {   // Remove old entries first    UnregisterServer();    // Add New entries    TCHAR szPath[MAX_PATH];    ::GetModuleFileName(m_hInstance, szPath,      sizeof(szPath)/sizeof(TCHAR));    HRESULT hr = AddRegistryEntry( "CLSID\\{318B4AD3-06A7-11d3-9B58-0080C8E11F14}\\InprocServer32",      "", szPath);    return hr;  }  // UnregisterServer - Removes entries from the system registry  HRESULT CMyModule::UnregisterServer(void)  {   DeleteRegistryKey(HKEY_CLASSES_ROOT,      "CLSID\\{318B4AD3-06A7-11d3-9B58-0080C8E11F14}");    return S_OK;  } 

The following code shows the final representation of class CMyModule:

 class CMyModule  { public:    CMyModule();    ~CMyModule();    // Initialize the object    HRESULT Init(HINSTANCE hInst);    // Cleanup the object    void Term();    // HINSTANCE access    HINSTANCE GetModuleInstance(){return m_hInstance; }    // Global count access    LONG Lock();    LONG Unlock();    LONG GetLockCount(){ return m_lCount; }    // Class object access    HRESULT GetClassObject(REFCLSID rclsid, REFIID riid,      LPVOID* ppv);    // Registration routines    HRESULT RegisterServer();    HRESULT UnregisterServer();    // Registry helper    static HRESULT AddRegistryEntry(LPCTSTR pszSubKey,      LPCTSTR pszValueName,      LPCTSTR pszValue);    static HRESULT DeleteRegistryKey(HKEY hKeyParent,      LPCTSTR pszSubKey);  private:    HINSTANCE m_hInstance;    CVcrClassObject* m_pVCRClassObject;    LONG m_lCount;  }; 

As the public data members are now declared as private, we will need to touch the rest of the server code to use appropriate methods instead of accessing data members directly. The following code snippet shows one such source file. This code defines all the entry points for the DLL.

 // File VcrDll.cpp  #include "StdAfx.h"  // DLL Entry Point  extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance,    DWORD dwReason, LPVOID /*lpReserved*/)  {   if (DLL_PROCESS_ATTACH == dwReason) {     g_MyModule.Init(hInstance);    }else    if (DLL_PROCESS_DETACH == dwReason) {     g_MyModule.Term();    }    return TRUE;  // ok  }  // Returns the requested interface for the requested clsid  STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid,    LPVOID* ppv)  {   return g_MyModule.GetClassObject(rclsid, riid, ppv);  }  // DllRegisterServer - Adds entries to the system registry  STDAPI DllRegisterServer(void)  {   return g_MyModule.RegisterServer();  }  // DllUnregisterServer - Removes entries from the system registry  STDAPI DllUnregisterServer(void)  {   return g_MyModule.UnregisterServer();  }  // Used to determine whether the DLL can be unloaded by OLE  STDAPI DllCanUnloadNow(void)  {   return (g_MyModule.GetLockCount()==0) ? S_OK : S_FALSE;  } 

We now have code that is more readable and easier to manage. Also, if we add a little more abstraction, we can use the same global class with every COM server that we write. This class isolates the mechanism to register, unregister, obtain the class object, and keep the global reference count.

Let s step back and start dissecting how COM objects are implemented.


< BACK  NEXT >


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
EAN: N/A
Year: 2000
Pages: 129

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net