Activating COM Objects

 < Free Open Study > 



COM objects do not sit alone on your hard drive as simple *.obj files. COM objects are packaged up inside some type of "component housing." Recall earlier in this chapter we examined the various physical relationships between a client and COM server (in-process, local, and remote). These three relationships were determined, in part, by the type of binary packaging containing the COM objects (DLL or EXE). Given that your COM objects need to live in some sort of binary house (which could be located outside of the client's process), the first question you might ask is, "How can a client create the objects living within another binary?" A perfect question to ask, as we now turn our attention to the standard way to create COM objects residing in a binary server.

You know COM is a language-independent architecture, and therefore a client cannot create a COM object using a language-specific keyword. For example, the C++ new operator has no built-in ability to create a new instance of a binary object. As we will see in Chapter 4, the Visual Basic New keyword appears to automatically load and extract an object from a binary server, but this is just a language mapping issue. Under the hood VB makes the same set of COM library calls that the C++ developer makes by hand. Furthermore, the VB New keyword and the C++ new operator may look the same; however, the semantics behind them are specific to the respective language. Clearly, language-specific constraints will not help out much in a language-independent architecture such as COM.

In addition, a COM client can create a server that may reside at any location in the enterprise. The promise of location transparency allows the same set of client-side COM code to activate a COM object located on the same machine as the client, a machine down the hall, or a machine across the state. Given these two issues (locality and language independence), we need a language- and location-neutral way in which a client can create a COM object. There is such a way through (you guessed it) another standard COM interface named IClassFactory.

COM Class Objects and IClassFactory

The definition of IClassFactory is also found in <unknwn.h>, and looks like the following:

// IClassFactory is defined in <unknwn.h>. MIDL_INTERFACE("00000001-0000-0000-C000-000000000046")      IClassFactory : public IUnknown      {      public:           virtual /* [local] */ HRESULT STDMETHODCALLTYPE CreateInstance(              /* [unique][in] */ IUnknown __RPC_FAR *pUnkOuter,              /* [in] */ REFIID riid,              /* [iid_is][out] */ void __RPC_FAR *__RPC_FAR *ppvObject) = 0;           virtual /* [local] */ HRESULT STDMETHODCALLTYPE LockServer(              /* [in] */ BOOL fLock) = 0;       };

IClassFactory specifies two methods that define a behavior for creating and monitoring COM objects in a language-independent and location-neutral manner. The core method is CreateInstance(), which will create a specific coclass on behalf of the calling client. LockServer() is used to hold the binary server itself in memory per client request (we will see why this can be helpful in just a moment). If we strip away the comments and standard COM macros, IClassFactory can be distilled down to the following:

// The core syntax of IClassFactory, stripped of comments and COM macros. interface IClassFactory : public IUnknown {      virtual HRESULT CreateInstance(LPUNKNOWN pUnk, REFIID riid,                                  void** ppv) = 0;      virtual HRESULT LockServer(BOOL fLock) = 0; };

It is important to point out that a secondary class factory interface, IClassFactory2, is also defined in COM. This interface extends the behavior of IClassFactory by supporting additional methods for licensing support. For example, you might have an ActiveX control that you do not want developers to use in their application until they provide (read: pay for) a valid *.lic (license) file. IClassFactory2 provides this functionality, and we will see details of this standard interface in Chapter 14. Now that we have an understanding of the behavior specified by IClassFactory(2), we can introduce a new COM term: the class object.

A class object is a COM object implementing the IClassFactory (or IClassFactory2) interface. Class objects are often called class factories.

Class objects exist only to create another type of COM object. This is how COM provides a language- and location-neutral means by which a client can create a coclass located in a binary server. If every COM-enabled language has some way to access the IClassFactory interface, every client is able to create the object they desire in a language-independent manner. Furthermore, as the actual implementation of the IClassFactory methods is hidden at the binary level, we (as the object creators) can use whatever language keywords we have at our disposal (such as the C++ new operator) to create the associated coclass. As they say, everything can be solved with another level of indirection!

COM class objects have a one-to-one correspondence with the object they are responsible for creating. In other words, every creatable COM object contained in a server has exactly one class object. Class objects are never responsible for creating a set of COM objects, no matter how related these objects appear to be. A server containing two creatable COM objects would break down like this:

click to expand
Figure 3-14: Class objects create related coclasses.

As suggested by Figure 3-14, class objects only implement the IUnknown and IClassFactory interfaces. Class objects never, ever implement interfaces supported by the object they are creating! The class factory simply creates the associated object, asks for a particular interface from the object, and returns it to the client. If you like, consider the COM class factory to be a language- and location-independent new operator.

Building a Class Factory

Let's develop a class object for the CoHexagon coclass defined earlier in this chapter. As you recall, CoHexagon implemented the IDraw and IShapeEdit interfaces. Our class factory, which we will call CoHexFactory, is responsible for creating CoHexagon objects for a client and returning some interface pointer from CoHexagon. The definition of CoHexFactory should appear straightforward:

// This class factory is in charge of creating CoHexagon objects. class CoHexFactory : public IClassFactory { public:      // constructor and destructor...      // IUnknown methods.      STDMETHODIMP QueryInterface(REFIID riid, void** pIFace);      STDMETHODIMP_(ULONG)AddRef();      STDMETHODIMP_(ULONG)Release();      // IClassFactory methods.      STDMETHODIMP CreateInstance(LPUNKNOWN pUnk, REFIID riid,                               void** pIFace);      STDMETHODIMP LockServer(BOOL fLock);     private:      ULONG m_refCount; }; 

As with any COM object, the implementation of AddRef() and Release() for a class factory will simply increment or decrement the internal reference counter, and check for the final release to remove itself from memory:

// Class objects, being COM objects, maintain a reference count. STDMETHODIMP_(ULONG) CoHexFactory::AddRef() {      return ++m_refCount; } STDMETHODIMP_(ULONG) CoHexFactory::Release() {      if(--m_refCount == 0)      {           delete this;           return 0;      }      return m_refCount; }

QueryInterface() will simply hand out pointers to the standard IUnknown or IClassFactory interfaces:

// Every single 'generic' class object will have a QueryInterface() implementation // which looks like the following. Again note that there are no provisions for // the interfaces supported by the related coclass (CoHexagon). STDMETHODIMP CoHexFactory::QueryInterface(REFIID riid, void** pIFace) {      if(riid == IID_IUnknown)           *pIFace = (IUnknown*)this;      else if(riid == IID_IClassFactory)           *pIFace = (IClassFactory*)this;      if(*pIFace){           ((IUnknown*)(*pIFace))->AddRef();           return S_OK;      }      *pIFace = NULL;      return E_NOINTERFACE; }

Implementing IClassFactory::CreateInstance()

So far, class objects look like any other COM object; however, class objects also support the IClassFactory interface. CreateInstance() is responsible for creating a new instance of the associated COM object, asking the object for the client-specified interface, and returning it to the client. As you examine the code behind CreateInstance(), think back to the global CarFactory() method you developed in the previous lab. You should be able to see the connection between the two. CarFactory() was also responsible for creating a coclass and returning an interface to the caller. However, as we are now interested in moving CoCar into the binary realm, we need a standard and well-known way for COM languages to create the object (after all, what does Visual Basic know of the C++ centric CarFactory() function?).

The first parameter of CreateInstance() is used in conjunction with COM aggregation. Recall that aggregation is one form of binary reuse in COM. We will not examine the details of aggregation until Chapter 8 and will assume this parameter to always be NULL (which specifies no aggregation support is being requested). The second parameter is the IID of the interface the client is interested in obtaining from the coclass once it has been created. As CoHexFactory creates CoHexagons, this parameter will more than likely be from the set {IID_IUnknown, IID_IDraw, IID_IShapeEdit}. The final parameter, void**, is a place to put the interface pointer fetched from the coclass. Without further ado, here is the implementation of CreateInstance():

// Looks a lot like the CarFactory(), does it not? STDMETHODIMP CoHexFactory::CreateInstance(LPUNKNOWN pUnk, REFIID riid, void** pIFace) {      // We do not support aggregation in this class object.      // If LPUNKNOWN is not NULL, return this standard HRESULT.      if(pUnk != NULL)           return CLASS_E_NOAGGREGATION;      // CoHexFactory makes CoHexagons.      CoHexagon* pHexObj = NULL;      HRESULT hr;      pHexObj = new CoHexagon;      // Ask object for an interface.      hr = pHexObj -> QueryInterface(riid, pIFace);      // Problem? We must delete the memory we allocated!      if (FAILED(hr))           delete pHexObj;      return hr; }

Implementing IClassFactory::LockServer()

Finally, we need to address the LockServer() method of IClassFactory in order to finish up our CoHexagon class factory. LockServer() provides a way for a client to lock the server down in memory, even if there are currently no active objects in the server. The reason to do so is client optimization. Once a client obtains an IClassFactory pointer, it may call LockServer(TRUE), which will bump up a global level lock counter maintained by the server. When the COM runtime attempts to unload a server from memory, this lock count is consulted first. If the value of the global lock counter is not zero (which signifies no locks), COM will just stop by later and ask again.

For example, as a COM client, you may wish to load up your COM servers when the application first comes to life, but delay the creation of the actual COM objects until the time they are specifically needed. To prevent having to reload the server, you may lock it down upon startup, even though you do not intend to create any objects at this time. Again, remember that LockServer() is nothing more than an optimization technique, and a way to keep a server loaded into memory even when it contains no living, breathing COM objects.

Any client that calls LockServer(TRUE) must call LockServer(FALSE) before terminating in order to decrement the server's global lock counter. With that said, assume that somewhere in the server code is a globally declared ULONG named g_lockCount. This variable is used to represent the number of existing client locks. The LockServer() method of CoHexFactory may then be implemented as such:

// LockServer() simply increments or decrements the server level global lock counter. STDMETHODIMP CoHexFactory::LockServer(BOOL fLock) {      if(fLock)                 ++g_lockCount;      else           --g_lockCount;      return S_OK; }

The next step is to package up CoHexagon and the CoHexFactory into a binary home.

Implementing DLL Component Housing

The last major hurdle facing us before CoHexagon is ready for client access is to create a binary home for itself and its class object to dwell. Assume you have created a Win32 DLL project workspace named Shapes, and wish to expose your COM objects from this in-process server. Every COM-based DLL gets its work done by exporting (through a standard DEF file) four necessary functions. Here is a breakdown of the functionality provided by each DLL export:

  • DllGetClassObject(): This method returns an IClassFactory pointer for a client, based off the CLSID of the object it is attempting to create. It is here that we create the class factory itself.

  • DllCanUnloadNow(): Remember the global lock counter? This is the function that will return "yes" or "no" to the question "Can I safely unload this DLL server from memory at this time?"

  • DllRegisterServer() and DllUnregisterServer(): Every well-behaved in-proc COM server should insert all necessary information into and out of the system registry when asked to do so. This is called self-registration of the server, and these two exports provide this very behavior.

As we have not yet looked into the COM runtime and the system registry, we will hold off on the details of DllRegisterServer() and DllUnregisterServer() for the moment, and concentrate on the other two required exports.

Exporting the Server's Class Objects: DllGetClassObject() and CLSIDs

The implementation of DllGetClassObject() creates a new class factory and returns the correct IClassFactory interface to the client. If your server contains a collection of coclasses, you will examine the incoming CLSID parameter of DllGetClassObject() to determine which class factory to create. This method has the following signature:

// Creates a given class factory for // the client based on the CLSID of the coclass. STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv); 

Assume shapes.dll contains two coclasses (CoSquare and CoDot). You may visualize DllGetClassObject() as the following:

click to expand
Figure 3-15: DllGetClass-Object() exposes class factories.

Every COM object contained in a binary package must be uniquely identified in the COM universe. In order to identify which coclass to create, we need to give our CoHexagon object a CLSID (which is simply a GUID). Again using the DEFINE_ GUID macro provided by guidgen.exe, we might specify CoHexagon as the following (the human readable constants of a coclass are prefixed with CLSID_ by convention):

// {F12327B0-DE3A-11d2-AAF4-00A0C9312D57} DEFINE_GUID(CLSID_CoHexagon, 0xf12327b0, 0xde3a, 0x11d2, 0xaa, 0xf4, 0x0, 0xa0, 0xc9, 0x31, 0x2d, 0x57);

Every single coclass in the server will need a unique CLSID. Do note that the class factory itself does not receive a CLSID. Here is an implementation of the first server export, DllGetClassObject():

// DllGetClassObject() is in charge of creating a class factory, and returning the // IClassFactory interface to the COM client. STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv) {      HRESULT hr;      CoHexFactory *pHFact = NULL;      // We only know how to make CoHexagon objects in this house.      if(rclsid != CLSID_CoHexagon)           return CLASS_E_CLASSNOTAVAILABLE;      // They want a CoHexagon, so create the hexagon factory.      pHFact = new CoHexFactory;           // Go get the interface from the Hex Factory (IClassFactory(2) or IUnknown)      hr = pHFact -> QueryInterface(riid, ppv);      if(FAILED(hr))           delete pHFact;      return hr; }

This implementation of DllGetClassObject() is quite simple, as shapes.dll contains a single coclass (CoHexagon) and therefore a single class factory named CoHexFactory. As you might imagine, if our COM DLL server contained 20 different coclasses, that would be 20 different class factories that DllGetClassObject() could create for the client. We could set up simple if/else logic to test the CLSID and create the correct class object. As we build more elaborate servers during the course of this book, you will be given a chance to do this very thing (with the welcome help of ATL).

Managing Server Lifetime: DllCanUnloadNow()

We know that a given COM object is in charge of deleting itself from memory when the last client releases the final interface pointer. For example, CoHexagon's implementation of Release(), like all COM objects, looks like the following:

// Every object maintains an internal reference counter. STDMETHODIMP_(ULONG) CoHexagon::Release() {      if(--m_refCount == 0)      {           delete this;           return 0;      }      else           return m_refCount; }

However, we also must provide a way for the server itself to "know" how many active objects are living in the binary house at any given time. Just because the server might have an active CoHexagon object inside, a DLL itself has no innate ability to understand that important fact. In other words, a DLL has no clue that a client has created a CoHexagon. We must inform the DLL of this fact in order for the DLL to stay in memory as long as there is some active object in the server. In short, we must provide the hooks.

A common way to inform your DLL that it has some active objects is to provide another global level counter, which identifies the number of active objects in the server at any given time. Whenever a coclass (CoHexagon) or class object (CoHexFactory) is created, the given constructors of these classes should bump up this global object counter variable. Whenever a coclass (CoHexagon) or class object (CoHexFactory) is terminated, the destructor should decrement this global object counter. Here is the revised CoHexagon class, which properly adjusts the server-wide object counter (CoHexFactory would also need to be retrofitted in the same way):

// Every COM object in your DLL server needs to 'tell' the DLL when it has // a new object coming to life, or when an object has died. // Assume that g_objCount has been defined in some external file. // Server gained an object. CoHexagon::CoHexagon() {      g_objCount++;     // Also increment in class factory. } // Server lost an object. CoHexagon::~CoHexagon {       g_objCount--;      // Also decrement in class factory. } 

So then, a given DLL has some global variables to worry about. As we have seen, one global counter maintains the number of outstanding locks. This global counter is adjusted by IClassFactory::LockServer(). The other global lock counter represents the number of active objects in the server at any given time, and is adjusted by the constructors and destructors of each and every COM object. Thus, a DLL can be unloaded safely by the COM runtime only if there are no server locks and no active objects. DllCanUnloadNow() can check the two global variables maintaining this information, and return S_OK or S_FALSE accordingly:

// The DllCanUnloadNow() server export informs the COM runtime when it is // safe to unload the DLL from memory. ULONG g_lockCount = 0;          // Modified by ICF::LockServer. ULONG g_objCount = 0;`          // Modified by ctor & dtor of any coclass in the server. STDAPI DllCanUnloadNow(void) {      if(g_lockCount == 0 && g_objCount == 0)           return S_OK;          // Unload me.      else           return S_FALSE;       // Keep me alive. }

Note 

Clients may force a call to DllCanUnloadNow() during idle item by invoking the COM library function CoFreeUnusedLibraries(). This call is not mandatory, but can be helpful to ensure our DLLs are not in memory longer than necessary.

You may wonder why we need to maintain two global counters in the DLL server. Could we not have a single global counter that is adjusted by LockServer() as well as the constructors and destructors of the objects? Sure, you bet. This is more or less a stylistic choice you can make as you develop your COM servers. In Chapter 5 we will take the "single counter in a server" approach. For now, we will stick with one counter for locking and another for the number of active objects, just to keep the details firm in your mind.

Exporting the Exports

Now that we have written DllGetClassObject() and DllCanUnloadNow(), we need to expose them to the outside world. To actually export these two DLL functions, you will need to assemble a standard Win32 DEF file, which must be included into your current project. The name of the library is the exact same name as your project workspace:

LIBRARY "SHAPES" EXPORTS      DllGetClassObject        @1     PRIVATE      DllCanUnloadNow          @2     PRIVATE

As long as we wrote all this code in the context of a Win32 DLL project workspace, we can compile this COM server! However, before a client could create and use the object, we first must enter the correct information into the system registry.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

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