Let's review the ISum interface definition we are attempting to implement in a coclass:
[ object, uuid(10000001-0000-0000-0000-000000000001) ] interface ISum : IUnknown { HRESULT Sum([in] int x, [in] int y, [out, retval] int* retval); } |
Although we can implement ISum using any language, such as Java or Visual Basic (we do both in Chapter 3), let's take this simple interface definition and implement it in C++. Recall the C++ definition of the ISum interface generated by MIDL in the component.h file:
MIDL_INTERFACE("10000001-0000-0000-0000-000000000001") ISum : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE Sum( /* [in] */ int x, /* [in] */ int y, /* [retval][out] */ int __RPC_FAR *retval) = 0; }; |
We call ISum an abstract base class because its four methods—three from IUnknown plus the Sum method declared above—are pure virtual functions, which means that they are unimplemented. To create a coclass in C++, the developer typically defines a C++ class deriving from the interfaces that the coclass will implement, as shown in the following code. Notice that the functions are no longer declared as pure (with the =0 specifier) because the CInsideCOM class intends to implement these methods.
class CInsideCOM : public ISum { public: // IUnknown ULONG __stdcall AddRef(); ULONG __stdcall Release(); HRESULT __stdcall QueryInterface(REFIID riid, void** ppv); // ISum HRESULT __stdcall Sum(int x, int y, int* retval); CInsideCOM() : m_cRef(1) { g_cLocks++; } ~CInsideCOM() { g_cLocks--; } private: ULONG m_cRef; // The reference counting variable }; |
Through reference counting, an object learns when it no longer has any clients and can therefore self-destruct. While the client sees reference counting as happening per-interface, objects use a single reference counter to keep track of all references regardless of what interface pointer is returned. To keep track of the current reference count, most COM+ objects maintain a private variable that is incremented and decremented by the IUnknown::AddRef and IUnknown::Release methods.
The CInsideCOM class's implementation of the AddRef method is exceedingly simple; its only job is to increment the private variable m_cRef. This variable is the actual reference counter for CInsideCOM objects. Here is the entire implementation of AddRef:
ULONG CInsideCOM::AddRef() { return ++m_cRef; } |
The component's implementation of the IUnknown::Release method obviously must decrement the m_cRef reference counter. However, the implementation is complicated by the fact that the reference counter might be decremented to 0, indicating that no one is using the object. When the reference counter reaches 0, the current instance of the CInsideCOM object is destroyed:
ULONG CInsideCOM::Release() { if(--m_cRef != 0) return m_cRef; delete this; return 0; } |
Notice that both AddRef and Release return the resulting value of the reference counter, which should be used for diagnostic or testing purposes only. If the client needs to know that resources have been freed, you must provide an interface with higher-level semantics.
QueryInterface is the most interesting of all the IUnknown methods. Recall that QueryInterface is the method used to retrieve pointers to the other interfaces supported by the object. This technique is sometimes called a sideways cast because the interfaces returned by QueryInterface are not related through an inheritance hierarchy. Here is a typical implementation of the IUnknown:: QueryInterface method:
HRESULT CInsideCOM::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IUnknown*)this; else if(riid == IID_ISum) *ppv = (ISum*)this; else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; } |
This implementation of QueryInterface simply compares the requested IID with every IID that the object supports. Because this object implements only IUnknown and ISum, only pointers to these interfaces can be returned. Notice that after the this reference has been cast to the requested interface pointer, the AddRef method is called. This optimization relieves the client from having to repeatedly call AddRef on each interface pointer obtained through QueryInterface. The client is required to call AddRef for every new copy of an interface pointer on a given object that is not obtained through QueryInterface.
It is interesting to note that the C++ comparison operator (==) is used to compare the IIDs. This comparison is possible because the objbase.h system header file overloads the comparison operator to perform the comparison using the IsEqualGUID function, as shown here:
__inline BOOL operator==(const GUID& guidOne, const GUID& guidOther) { return IsEqualGUID(guidOne,guidOther); } |
As you know by now, a C++ class is not equivalent to a COM+ class. Before you build COM+ classes out of the basic construction material provided by C++, you should know about an important area of mismatch between C++ and COM+ that must be overcome, which relates to their type systems. The basic implementation of the IUnknown::QueryInterface method shown previously uses C-style casts to convert the this reference in C++ to a COM+ interface pointer. While this is acceptable, it is better to use C++ features to help bridge the gap between C++ and COM+ type systems. You can use the static_cast operator as a replacement for C-style casts in the QueryInterface method. Consider the following code fragment:
class CCar : public ICar HRESULT CCar::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IUnknown*)this; else if(riid == IID_ISum) *ppv = (ISum*)this; |
In this fragment, ISum is not a base class of the CCar class, but the C++ type system does not flag an error; the compiler assumes that we know what we're doing because we have employed the explicit C-style cast.8 By replacing the C-style cast with the static_cast operator, as shown below, the compiler helps us catch this error before it causes run-time havoc. The static_cast operator does the conversion based solely on the types present in the expression; no run-time type checking is involved.
class CCar : public ICar HRESULT CCar::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IUnknown*)this; else if(riid == IID_ISum) *ppv = static_cast<ISum*>(this); // Compile-time error |
The IUnknown interface is the most fundamental interface pointer you can have to an object; it establishes the identity of the object. This is important because in order for the remoting architecture of COM+ to be able to work its magic, the unique identity of each object must be preserved. As a consequence of the need to preserve identity in COM+, you must adhere to several rules regarding implementation of the IUnknown::QueryInterface method. While it might seem somewhat esoteric, understanding the implications of identity in COM+ will help you understand the entire architecture, which is based on the following fundamental ideas:
Objects must support static interface sets The set of interfaces accessible through QueryInterface must be static, not dynamic. If a call to QueryInterface for a specific interface pointer succeeds the first time, it must succeed again, and if it fails the first time, it must fail on all subsequent attempts. Thus, given one object's IUnknown interface pointer named pUnknown, the following code must execute successfully:
ISum* pSum1 = 0; ISum* pSum2 = 0; // Ask the object (referenced by pUnknown) for any one // interface pointer two times. HRESULT hr1 = pUnknown->QueryInterface(IID_ISum, (void**)&pSum1); HRESULT hr2 = pUnknown->QueryInterface(IID_ISum, (void**)&pSum2); // Both attempts must either succeed or fail if(!(SUCCEEDED(hr1) == SUCCEEDED(hr2))) cout << "Error: Objects must support a static set of interfaces." << endl; // Release the references added by QueryInterface. if(SUCCEEDED(hr1) && SUCCEEDED(hr2)) { pSum1->Release(); pSum2->Release(); } |
IUnknown must be unique QueryInterface must always return the exact same memory address for each client request for a pointer to the IUnknown interface.9 This means that you can always determine whether two pointers are pointing to the same object simply by comparing their IUnknown interface pointers; if they both point to the same address, they refer to the same object. Thus, given one object's IUnknown interface pointer named pUnknown, the following code must execute successfully:
IUnknown* pUnknown1 = 0; IUnknown* pUnknown2 = 0; // Ask the object (referenced by pUnknown) for its IUnknown interface // pointer two times. HRESULT hr1 = pUnknown>QueryInterface(IID_IUnknown, (void**)&pUnknown1); HRESULT hr2 = pUnknown>QueryInterface(IID_IUnknown, (void**)&pUnknown2); // Both attempts must succeed. if(FAILED(hr1) || FAILED(hr2)) cout << "IUnknown must always be available." << endl; // The two pointer values must be identical. if(pUnknown1 != pUnknown2) cout << "Error: Object identity rules have been violated!" << endl; // Release the references added by QueryInterface. if(SUCCEEDED(hr1) && SUCCEEDED(hr2)) { pUnknown1->Release(); pUnknown2->Release(); } |
QueryInterface must be reflexive The reflexive rule requires that if a client holds a pointer to an interface and queries for that same interface, the call must succeed. Thus, given one object's IUnknown interface pointer named pUnknown, the following code must execute successfully:
ISum* pSum1 = 0; ISum* pSum2 = 0; // Ask the object for an interface pointer. HRESULT hr1 = pUnknown->QueryInterface(IID_ISum, (void**)&pSum1); if(SUCCEEDED(hr1)) { // Assuming that was successful, ask for the same interface // using the interface pointer retrieved above. HRESULT hr2 = pSum1->QueryInterface(IID_ISum, (void**)&pSum2); // The second request for the same interface must // also succeed. if(FAILED(hr2)) cout << "Reflexive rule of QueryInterface violated." << endl; // Release the references added by QueryInterface. pSum1->Release(); if(SUCCEEDED(hr2)) pSum2->Release(); } |
QueryInterface must be symmetric The symmetric rule requires that if a client holds a pointer to one interface and queries successfully for a second interface, then the client must be able to call QueryInterface through the second pointer for the first interface. Thus, given one object's IUnknown interface pointer named pUnknown, the following code must execute successfully:
IOne* pOneA = 0; // Ask the object for an interface pointer. HRESULT hr1 = pUnknown->QueryInterface(IID_IOne, (void**)&pOneA); if(SUCCEEDED(hr1)) { ITwo* pTwo = 0; // Assuming that was successful, ask for another interface // using the interface pointer retrieved above. HRESULT hr2 = pOneA->QueryInterface(IID_ITwo, (void**)&pTwo); if(SUCCEEDED(hr2)) { IOne* pOneB = 0; // Assuming that was successful, ask for the first // interface again using the interface retrieved in // the preceding step. HRESULT hr3 = pTwo->QueryInterface(IID_IOne, (void**)&pOneB); // The second request for the IOne interface // must succeed. if(FAILED(hr3)) cout << "Symmetric rule of QueryInterface violated." << endl; // Release the references added by QueryInterface. pTwo->Release(); if(SUCCEEDED(hr3)) pOneB->Release(); } pOneA->Release(); } |
QueryInterface must be transitive The transitive rule requires that if a client holds a pointer to one interface, queries successfully for a second interface, and through that pointer queries successfully for a third interface, then a query for the third interface through the pointer for the first interface must also succeed. Thus, given one object's IUnknown interface pointer named pUnknown, the following code must execute successfully:
IOne* pOne = 0; // Ask the object for an interface pointer. HRESULT hr1 = pUnknown->QueryInterface(IID_IOne, (void**)&pOne); if(SUCCEEDED(hr1)) { ITwo* pTwo = 0; // Assuming that was successful, ask for a second interface // using interface pointer retrieved above. HRESULT hr2 = pOne->QueryInterface(IID_ITwo, (void**)&pTwo); if(SUCCEEDED(hr2)) { IThree* pThreeA = 0; // Assuming that was successful, ask for a third // interface using interface pointer retrieved in the // preceding step. HRESULT hr3 = pTwo->QueryInterface(IID_IThree, (void**)&pThreeA); if(SUCCEEDED(hr3)) { IThree* pThreeB = 0; // Assuming that was successful, ask for the third // interface using interface pointer retrieved in // the first step. HRESULT hr4 = pOne->QueryInterface(IID_IThree, (void**)&pThreeB); // This request must succeed. if(FAILED(hr4)) cout << "Transitive rule of QueryInterface violated." << endl; // Release the references added by QueryInterface. if(SUCCEEDED(hr4)) pThreeB->Release(); pThreeA->Release(); } pTwo->Release(); } pOne->Release(); } |
The sample implementation of the IUnknown::QueryInterface method shown previously meets all of the requirements. Figure 2-4 shows the effects of the symmetric, reflexive, and transitive rules of QueryInterface.
Figure 2-4. The reflexive, symmetric, and transitive rules of QueryInterface.
The last method to be implemented is the Sum method, the sole method of the ISum interface. The implementation of this method should not cause any surprise:
HRESULT CInsideCOM::Sum(int x, int y, int* retval) { *retval = x + y; return S_OK; } |
Perhaps you're wondering what really happens when the client calls CoCreate- Instance. How does CoCreateInstance return that first pointer to an object's IUnknown implementation? This is an excellent question, but unfortunately the answer is a bit complicated. In our sample, CoCreateInstance needs only to create an instance of the CInsideCOM class that implements ISum. This could be as simple as using the C++ new operator, and in fact the new operator is ultimately used. But here is the place to recall that one of the central tenets of COM+ is the concept of location transparency, which states that clients should be able to reach objects easily, regardless of whether they are in-process, local, or remote components. Assuming that our object is running in-process, you could use the new operator simply to create an instance of the CInsideCOM class. But what if that component is running in a separate process address space or on a remote computer? The new operator is obviously not the whole answer.
COM+'s answer to this issue is IClassFactory, which is an interface implemented by COM+ objects that manufacture other COM+ objects. For this reason, the name IClassFactory is a misnomer; a much more descriptive name would be IObjectFactory. Be that as it may, most coclasses have an associated class factory that implements the IClassFactory interface. The class factory, in turn, enables clients to instantiate the desired coclass using a helper function such as CoCreateInstance; in fact, CoCreateInstance calls methods of the IClassFactory interface internally. The term class factory describes creation objects that implement the IClassFactory interface. The more general term for this type of object is a class object, which refers to any creation object regardless of what interfaces it might implement. Since most class objects implement the IClassFactory interface, this term is often used synonymously with class factories. Figure 2-5 shows how a coclass is instantiated by a class factory.
Figure 2-5. A class factory creating instances of a coclass.
In the unknwn.idl file, IClassFactory is defined as follows:
interface IClassFactory : IUnknown { HRESULT CreateInstance([in, unique] IUnknown* pUnkOuter, [in] REFIID riid, [out, iid_is(riid)] void** ppvObject); HRESULT LockServer([in] BOOL fLock); } |
As you can see, IClassFactory has two methods: CreateInstance and LockServer. The CreateInstance method actually instantiates a COM+ object; the LockServer method prevents the component from unloading while a client is still using one of its objects.
Here is the declaration of the C++ class CFactory, which implements the IClassFactory interface for the purpose of instantiating CInsideCOM objects:
class CFactory : public IClassFactory { public: // IUnknown ULONG __stdcall AddRef(); ULONG __stdcall Release(); HRESULT __stdcall QueryInterface(REFIID riid, void** ppv); // IClassFactory HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter, REFIID riid, void** ppv); HRESULT __stdcall LockServer(BOOL bLock); CFactory() : m_cRef(1) { g_cLocks++; } ~CFactory() { g_cLocks--; } private: ULONG m_cRef; }; |
IUnknown must be implemented again because all COM+ objects must support this interface. We won't bore you with the implementation of AddRef and Release because these simply increment and decrement the m_cRef reference counter. The IUnknown::QueryInterface method, shown below, is also implemented in the standard way; client requests for either the IUnknown or IClassFactory interfaces are honored with the desired interface pointer to the class object. Note that the object's reference counter is incremented via the AddRef method before any interface pointers are returned.
HRESULT CFactory::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IUnknown*)this; if(riid == IID_IClassFactory) *ppv = (IClassFactory *)this; else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; } |
The IClassFactory::CreateInstance method is invoked when the client calls the CoCreateInstance function to instantiate an object. Here is a sample implementation of the IClassFactory::CreateInstance method:
HRESULT CFactory::CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv) { if(pUnknownOuter != NULL) return CLASS_E_NOAGGREGATION; CInsideCOM *pInsideCOM = new CInsideCOM; if(pInsideCOM == NULL) return E_OUTOFMEMORY; HRESULT hr = pInsideCOM->QueryInterface(riid, ppv); pInsideCOM->Release(); // In case QueryInterface fails return hr; } |
The first parameter to CreateInstance tells us whether the client is instantiating the object as part of an aggregate object. Recall that when the client code calls CoCreateInstance, its second parameter specifies aggregate information; this is where the value of the pUnknownOuter parameter of IClassFactory::CreateInstance comes from. This particular component doesn't support aggregation, so to the client that wants to create the object as part of an aggregate, we return the error CLASS_E_NOAGGREGATION. (We'll discuss the identity tricks made possible by aggregate objects in the section titled "Aggregation.")
CreateInstance actually manufactures a spanking new CInsideCOM object using the C++ new operator. CreateInstance knows which coclass to instantiate because every externally exposed coclass has its own associated class object. Assuming that all is well, we now call CInsideCOM::QueryInterface (not CFactory::QueryInterface) to get the interface requested by the client in the fourth parameter of CoCreateInstance. (This is provided to the component as the second parameter [riid] of the IClassFactory::CreateInstance method.) In the case of this class object, only the IUnknown and IClassFactory interfaces are supported. Recall that inside the QueryInterface method, AddRef is called to increment the reference counter, so the client doesn't need to call AddRef on the interface pointer returned by CoCreateInstance.
The phantom reference The call to the Release method immediately following the QueryInterface call in the preceding code fragment might seem rather odd. If QueryInterface does us the favor of calling AddRef, why should we turn around and call Release? The reasoning behind this seemingly inexplicable behavior arises from the problem of what to do if the QueryInterface call fails. QueryInterface might fail simply because the client requested an interface not supported by the object. If that happens, AddRef is not called, but the component is left with an object that has no client, an object with a reference count of 0, and an object that will never be destroyed. Obviously, this is not a good situation.
To handle this potential catastrophe, the object's constructor initializes the value of the reference counter variable, m_cRef, to 1. This setting is artificial because at the time of construction, no client yet has a reference to the object. The counter is set to 1 so that if the QueryInterface call fails, the subsequent Release call in the IClassFactory::CreateInstance method will decrement the reference counter to 0. The Release method's implementation will realize that the object has no remaining clients (m_cRef == 0) and will destroy the object. Although initially somewhat confusing, this defensive programming practice deals cleanly with the problem of a failed QueryInterface call with a minimum of overhead. When QueryInterface succeeds, the extra Release call does no damage because it simply undoes the phantom reference count set by the object's constructor.
Artificial reference counts Another common defensive programming technique you can use when implementing or using COM+ objects is called artificial reference counts. If, while writing code that uses an object, you call a function and pass that object as a parameter, the function might Release the object, causing premature destruction of the object and failure of your code on return from the function. By inserting a call to AddRef before passing the object to the function, and then Release on its return, you artificially increment the object's reference counter, thereby ensuring that it will not be destroyed.
Although it is simple enough to implement the IClassFactory::LockServer method in in-process components, this method was designed primarily for executable components, which have an unusual cyclical reference counting problem with class objects. Executable components typically provide the COM+ marshaling infrastructure with a reference to their class objects on startup. (For information about how an executable component registers its class objects with COM+ , see the section titled "Building an ExecutableComponent" in Chapter 13.) An executable component that relies on the class object's reference counter, m_cRef, to determine when to unload will never exit because there will always be at least one outstanding reference to each of the component's class objects. To avoid this catch-22 situation, use the IClassFactory::LockServer method to lock an executable component in memory while outstanding references to the class objects are extant. (For more information about this method, see the section titled "Managing the Lifetime of an Executable Component" in Chapter 13.)
Because of the cyclical reference counting problem in executable components, developers did not bother to count client references for class objects in the modulewide lock counter (g_cLocks) that prevents components from unloading. Instead, they used the IClassFactory::LockServer method for this purpose when implementing both in-process and executable components. This was ineffective for both in-process and executable components because most clients never called the IClassFactory::LockServer method to begin with. This error was at least partly the fault of the documentation for the IClassFactory::LockServer method, which inaccurately claims that LockServer is "called by the client of a class object to keep a server open in memory, allowing instances to be created more quickly" [our italics]. LockServer was designed to meet the critical need for component lifetime management in COM+, not as an optimization that squeezes extra cycles from a component at run time.
The LockServer method was also subject to a particularly nasty race condition in early versions of COM+, which has been corrected for executable components. (For more about this race condition, see the section titled "Race Conditions" in Chapter 13.) In-process components are not vulnerable to this race condition, but they still do not need to rely on the IClassFactory::LockServer method. Taken together, these problems meant that components relying on the IClassFactory::LockServer method for lifetime management sometimes unloaded while a client held only a pointer to a component's class object; this was obviously unacceptable because if the client attempted to use this invalid pointer, an access violation resulted.
In-process components do not provide the COM+ marshaling infrastructure with a pointer to their class objects; such pointers are retrieved via the DllGetClassObject function (which is discussed in detail later in this chapter). Thus, in-process components can safely use the class object's reference counter, m_cRef, to determine when all the class objects have been freed, thereby allowing the component to unload without wreaking havoc on unsuspecting clients. This solution leaves in-process components with little need for the IClassFactory::LockServer method.
Although the previous discussion makes clear that the LockServer method is irrelevant to in-process components, the fact that the component implements the IClassFactory interface mandates that we implement this method. The implementation of the LockServer method is fairly straightforward. Based on the Boolean parameter bLock, this method either increments or decrements the modulewide lock counter variable named g_cLocks, as shown here:
long g_cLocks = 0; HRESULT CFactory::LockServer(BOOL bLock) { if(bLock) g_cLocks++; else g_cLocks--; return S_OK; } |
We've almost completed our journey through the code required to build a COM+ object. Before we go on, we need to decide how to package this object. Do we want to create an executable (EXE) component or an in-process (DLL) component? Executable components require a main or WinMain function; in-process components require two helper functions, DllGetClassObject and DllCanUnloadNow. For this first attempt at building a COM+ component, let's create an in-process component. (For information on building executable components, see Chapter 13.) Don't worry about having to implement those two extra functions—it's no big deal.
In Windows, DLLs are controlled by three primary functions: LoadLibrary(Ex), GetProcAddress, and FreeLibrary. You use the Win32 API function LoadLibrary(Ex) to load a DLL into the caller's address space. You use GetProcAddress to retrieve pointers to the functions exported by the DLL, enabling clients to access its services. When a client has finished using the services offered by the DLL, the DLL is freed by a call to FreeLibrary. The FreeLibrary function decrements the library's usage counter, unloading the library when the counter reaches 0.
COM+ helps you transition to an object-oriented world of components and work with them in a consistent manner. On some level, however, COM+ is just a shiny veneer that sits on top of Windows; an in-process COM+ component is just a fancy name for a Windows DLL. To call the methods of a COM+ object housed in a DLL, we need to obtain the address where it is loaded in memory. The IUnknown::QueryInterface method is designed to return interface pointers, but we can only call the QueryInterface method once we have an IUnknown interface pointer. How do we get that first pointer to IUnknown? This is where DllGetClassObject comes in. The DllGetClassObject function is not a method of any interface; it is a fossilized exported function that gives us that first pointer.
As you know, each class factory creates only one type of object, while a typical component might contain multiple coclasses, each of which requires its own class factory. The purpose of DllGetClassObject is to direct us to the correct class factory for the type of object we want to create. Since our sample component has only one supported coclass (CLSID_InsideCOM), our implementation of DllGetClassObject is rather rudimentary:
HRESULT __stdcall DllGetClassObject(REFCLSID clsid, REFIID riid, void** ppv) { if(clsid != CLSID_InsideCOM) return CLASS_E_CLASSNOTAVAILABLE; // Tough luck! CFactory* pFactory = new CFactory; if(pFactory == NULL) return E_OUTOFMEMORY; // riid is probably IID_IClassFactory. HRESULT hr = pFactory->QueryInterface(riid, ppv); pFactory->Release(); // Just in case QueryInterface fails return hr; } |
DllGetClassObject first checks to see whether the client has requested the CLSID_InsideCOM coclass. If it hasn't, we simply return the error CLASS_E_CLASSNOTAVAILABLE. If the client requests a class object supported by this component, DllGetClassObject instantiates a class factory using the C++ new operator. After that, we call QueryInterface to ask the class factory for a pointer to its IClassFactory interface. The third parameter of DllGetClassObject returns the IClassFactory interface pointer back to the client. Notice once again that the Release method is called immediately after the QueryInterface call. Remember that this call works to cleanly deallocate the CFactory object in case the QueryInterface call fails.
You might be wondering why DllGetClassObject even bothers with the IID parameter of the IClassFactory interface. After all, if all objects are instantiated by a class factory, why not simply hard-code IID_IClassFactory and rename the DllGetClassObject function DllGetClassFactory? The answer is that some objects might have special requirements not met by IClassFactory and might want to implement a custom activation interface. A custom activation interface is useful when a standard implementation of IClassFactory just won't do. For example, suppose you want to support licensing to restrict your component to machines on which it was properly installed. IClassFactory does not offer this functionality, but an improved version of the interface, IClassFactory2, does. The IDL definition of the IClassFactory2 interface is shown below:
interface IClassFactory2 : IClassFactory { typedef IClassFactory2* LPCLASSFACTORY2; typedef struct tagLICINFO { LONG cbLicInfo; BOOL fRuntimeKeyAvail; BOOL fLicVerified; } LICINFO; typedef struct tagLICINFO* LPLICINFO; // Fills a caller-allocated LICINFO structure with // information describing the licensing capabilities of // this class factory HRESULT GetLicInfo([out] LICINFO* pLicInfo); // Creates and returns a license key HRESULT RequestLicKey([in] DWORD dwReserved, [out] BSTR * pBstrKey); // Creates an instance of the object class supported by this // class factory, given a license key HRESULT CreateInstanceLic([in] IUnknown* pUnkOuter, [in] IUnknown* pUnkReserved, [in] REFIID riid, [in] BSTR bstrKey, [out, iid_is(riid)] PVOID* ppvObj); } |
In rare cases in which neither IClassFactory nor IClassFactory2 is sufficient, a custom activation interface might be the only solution. (This advanced topic is covered in Chapter 11.) The client then passes the IID parameter of the custom activation interface to the CoGetClassObject function. Note that if a class object does not implement IClassFactory, clients cannot instantiate objects using CoCreateInstance because that function automatically queries for the IClassFactory interface.
The last function we need for our sample component is DllCanUnloadNow. This function determines whether the DLL is in use, based on the g_cLocks modulewide lock counter maintained by the component. This lock counter keeps track of the number of class objects, objects, and IClassFactory::LockServer calls. If the lock counter reaches 0, the component does not have any valid clients, so DllCanUnloadNow returns S_OK and the caller can safely unload the DLL from memory.10 Otherwise, it returns S_FALSE to indicate that the DLL should not be unloaded. A standard implementation of DllCanUnloadNow is shown below:
HRESULT __stdcall DllCanUnloadNow() { if(g_cLocks == 0) return S_OK; else return S_FALSE; } |
Notice that you do not have to call DllCanUnloadNow directly—it is called by the CoFreeUnusedLibraries function. Client applications can occasionally call CoFreeUnusedLibraries to unload DLLs that are no longer in use. CoFreeUnusedLibraries walks an internal list of loaded in-process components and queries each DLL about its status by calling the DllCanUnloadNow function. If a component returns S_OK, CoFreeUnusedLibraries calls the CoFreeLibrary function to physically unload the DLL. In-process components that do not implement DllCanUnloadNow are unloaded only when the client process calls CoUninitialize (usually just before exiting). The CoUninitialize function unloads all in-process COM+ components.
You might wonder how the CoCreateInstance function actually works its magic. It actually calls another function, CoGetClassObject, which provides an interface pointer to the requested class object associated with a specified CLSID. The following code fragment shows the CoGetClassObject function in action. If necessary, CoGetClassObject dynamically loads the code required to obtain the class object interface pointer, even if that code happens to be on a remote machine. It checks the registry entries for the requested CLSID to learn where the component can be found.
hr = CoGetClassObject(CLSID_InsideCOM, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void**)&pClassFactory); if(FAILED(hr)) cout << "CoGetClassObject failed. " << endl; |
Normally, CoGetClassObject retrieves a pointer to an object's implementation of IClassFactory, although some other activation interface might be requested. CoCreateInstance retrieves the IClassFactory interface pointer so that it can call the IClassFactory::CreateInstance method to manufacture a COM+ object, as shown here:
hr = pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnknown); if(FAILED(hr)) cout << "pClassFactory->CreateInstance failed." << endl; |
Once CoCreateInstance finishes creating the object using the class factory, the class factory can be released, as shown here:
pClassFactory->Release(); |
If you prefer, you can replace the call to CoCreateInstance in the client with equivalent code that uses CoGetClassObject directly. Is there a reason for doing this? Absolutely. CoGetClassObject provides greater control over how and when objects are instantiated and has lower overhead than multiple calls to CoCreateInstance. Since you can use CoGetClassObject to return a pointer to an object's IClassFactory implementation, clients that want to create several objects of the same coclass should use a single call to CoGetClassObject followed by multiple calls to the IClassFactory::CreateInstance method instead of multiple calls to CoCreateInstance. The code on the following page replaces CoCreateInstance in the client project with equivalent code that uses CoGetClassObject.
IClassFactory* pClassFactory; CoGetClassObject(CLSID_InsideCOM, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void**)&pClassFactory); pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnknown); pClassFactory->Release(); |
In fact, you can even instantiate and access an in-process COM+ object without using any COM+ functions. The following C++ code loads an in-process component, obtains a pointer to the DllGetClassObject function, and calls IClassFactory::CreateInstance—all without using a single COM+ API function.11
IUnknown* pUnknown; IClassFactory* pClassFactory; // Load the DLL. HINSTANCE myDLL = LoadLibrary("C:\\component.dll"); // Declare a pointer to the DllGetClassObject function. typedef HRESULT (__stdcall *PFNDLLGETCLASSOBJECT)(REFCLSID clsid, REFIID riid, void** ppv); // Get a pointer to the component's DllGetClassObject function. PFNDLLGETCLASSOBJECT DllGetClassObject = (PFNDLLGETCLASSOBJECT)GetProcAddress(myDLL, "DllGetClassObject"); // Call DllGetClassObject to get a pointer to the class factory. DllGetClassObject(CLSID_InsideCOM, IID_IClassFactory, (void**)&pClassFactory); // IClassFactory::CreateInstance and IUnknown::Release pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnknown); pClassFactory->Release(); |
When activating an in-process component, CoGetClassObject loads the DLL by calling the CoLoadLibrary function, which is just a wrapper around the standard Win32 LoadLibrary function. Then it calls GetProcAddress to retrieve the address of the DllGetClassObject function exported by the DLL. The address returned from GetProcAddress is used to call the DllGetClassObject function and retrieve an interface pointer to the class object, magically leading us into the world of COM+. Figure 2-6 puts in perspective the functions and the order in which they are called when activating an in-process component.
Figure 2-6. A client calling CoCreateInstance to instantiate an in-process COM+ object.
In reality, CoGetClassObject delegates the task of locating and loading the component to the Service Control Manager (SCM—pronounced "scum")12 implemented by the rpcss.dll system process. After the SCM locates and loads the requested component, COM+ and the SCM drop out of the picture, allowing the client and the component to communicate directly. In the case of unconfigured COM+ components (which do not run in the COM+ run-time environment), COM+ does not insert a mediator between in-process components and their clients that could hurt performance; in-process components configured to execute in the COM+ run-time environment pay a price for the extra services provided. For executable and remote components, the COM+ marshaling infrastructure is used to transfer parameters and function invocations between the client and component. When an object is invoked on a remote machine, the SCM on the local machine contacts the SCM on the remote machine to request that it locate and load the component.
Before we build the component, we must create a module definition (.def) file that lists the exported functions, as shown in Listing 2-3. The DllGetClassObject and DllCanUnloadNow functions are exported for use only by COM+. Placing them in the import library can lead to unusual behavior if a program linked to the library incorrectly makes calls to them. Therefore, we must use the optional PRIVATE keyword to prevent the function names from being placed in the import library generated by the linker; it has no effect on the export table in the DLL.
component.def
LIBRARY component.dll DESCRIPTION '(c)1999 Guy Eddon' EXPORTS DllGetClassObject @1 PRIVATE DllCanUnloadNow @2 PRIVATE |
Listing 2-3. The module definition file listing the exported functions DllGetClassObject and DllCanUnloadNow.
To build the component project, open the InProcess.dsw workspace file in the Samples\The IUnknown Interface folder on the companion CD. Then build the project named Component. The complete component.cpp code is shown in Listing 2-4.
component.cpp
#include "Component\component.h" // Generated by MIDL // {10000002-0000-0000-0000-000000000001} const CLSID CLSID_InsideCOM = {0x10000002,0x0000,0x0000, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}}; long g_cLocks = 0; class CInsideCOM : public ISum { public: // IUnknown ULONG __stdcall AddRef(); ULONG __stdcall Release(); HRESULT __stdcall QueryInterface(REFIID riid, void** ppv); // ISum HRESULT __stdcall Sum(int x, int y, int* retval); CInsideCOM() : m_cRef(1) { g_cLocks++; } ~CInsideCOM() { g_cLocks--; } private: ULONG m_cRef; }; ULONG CInsideCOM::AddRef() { return ++m_cRef; } ULONG CInsideCOM::Release() { if(--m_cRef != 0) return m_cRef; delete this; return 0; } HRESULT CInsideCOM::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IUnknown*)this; else if(riid == IID_ISum) *ppv = (ISum*)this; else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; } HRESULT CInsideCOM::Sum(int x, int y, int* retval) { *retval = x + y; return S_OK; } class CFactory : public IClassFactory { public: // IUnknown ULONG __stdcall AddRef(); ULONG __stdcall Release(); HRESULT __stdcall QueryInterface(REFIID riid, void** ppv); // IClassFactory HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter, REFIID riid, void** ppv); HRESULT __stdcall LockServer(BOOL bLock); CFactory() : m_cRef(1) { g_cLocks++; } ~CFactory() { g_cLocks--; } private: ULONG m_cRef; }; ULONG CFactory::AddRef() { return ++m_cRef; } ULONG CFactory::Release() { if(--m_cRef != 0) return m_cRef; delete this; return 0; } HRESULT CFactory::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IUnknown*)this; else if(riid == IID_IClassFactory) *ppv = (IClassFactory*)this; else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; } HRESULT CFactory::CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv) { if(pUnknownOuter != NULL) return CLASS_E_NOAGGREGATION; CInsideCOM *pInsideCOM = new CInsideCOM; if(pInsideCOM == NULL) return E_OUTOFMEMORY; HRESULT hr = pInsideCOM->QueryInterface(riid, ppv); pInsideCOM->Release(); return hr; } HRESULT CFactory::LockServer(BOOL bLock) { if(bLock) g_cLocks++; else g_cLocks --; return S_OK; } HRESULT __stdcall DllCanUnloadNow() { if(g_cLocks == 0) return S_OK; else return S_FALSE; } HRESULT __stdcall DllGetClassObject(REFCLSID clsid, REFIID riid, void** ppv) { if(clsid != CLSID_InsideCOM) return CLASS_E_CLASSNOTAVAILABLE; CFactory* pFactory = new CFactory; if(pFactory == NULL) return E_OUTOFMEMORY; // riid is probably IID_IClassFactory. HRESULT hr = pFactory->QueryInterface(riid, ppv); pFactory->Release(); return hr; } |
Listing 2-4. The complete component code.
After we build the client and component, we are almost ready to run and test the code. First, however, the correct entries must be placed in the Windows registry so that the client can locate the component. The simplest way to arrange for the creation of registry entries is by using a registration (.reg) file, as shown in Listing 2-5.
component.reg
REGEDIT4 [HKEY_CLASSES_ROOT\CLSID\{10000002-0000-0000-0000-000000000001}] @="Inside COM+: In Process Component" [HKEY_CLASSES_ROOT\CLSID\{10000002-0000-0000-0000-000000000001}\ InprocServer32] @="C:\\Component.dll" |
Listing 2-5. The component registration file.
The registry file shown here provides the entries necessary for the client to activate the component built in this chapter. You might need to adjust the path of the InprocServer32 key to the folder where your component.dll file was built. To add this information to the registry, simply double-click on the registration file or execute the following command at the command line:
C:\WINDOWS>regedit c:\component.reg |
Registration files are a useful and relatively easy way to automate the addition of items to the registry. Most commercial software vendors, however, prefer that their software packages have as few unneeded files as possible. Registration files are also not terribly flexible. They do not offer a ready-made solution if an application's setup program decides that due to the configuration of the user's computer, different registry settings should be created. A better way is to create a self-registering component, which does not rely on an external registration file to be properly registered. Like plug-and-play devices that carry with them all the necessary configuration information, self-registering components carry with them all the code required to create the necessary registry entries.
In-process components and executable components have slightly different self-registration mechanisms. For in-process components, self-registration means that the component must export two additional functions, DllRegisterServer and DllUnregisterServer. A setup program can load the DLL and call these functions to instruct the component to register itself. You can also use the RegSvr32.exe utility that comes with Windows to call these exported functions, as shown here:
C:\WINDOWS\SYSTEM>regsvr32 c:\component.dll |
An alternative when you work in Visual C++ is to choose Register Control from the Tools menu. This feature uses the RegSvr32.exe utility to load the DLL and call its DllRegisterServer function.
Self-registering executable components do not export the DllRegisterServer and DllUnregisterServer functions; they examine their command-line parameters for the /RegServer or /UnregServer flags when they are executed. Some self-registering executable components automatically reregister themselves at startup. (For more on building self-registering executable components, see the section titled "Building an Executable Component" in Chapter 13.)
At run time, registry entries can be created using the Win32 API functions that manipulate the registry, including RegCreateKeyEx, RegOpenKeyEx, RegEnumKeyEx, RegSetValueEx, RegCloseKey, and RegDeleteKey.13 To automate the rather dreary process of creating and removing the typically needed registry entries, we have written a module that makes the low-level calls to these registry functions. The registry.cpp file containing the C++ source code is on the companion CD. The registry.h file, shown in Listing 2-6, declares the four main functions of the module, RegisterServer, UnregisterServer, RegisterServerEx, and UnregisterServerEx. These high-level functions can be called from any component to add and remove standard registry entries.
registry.h
// This function will register a component. HRESULT RegisterServer(const char* szModuleName, REFCLSID clsid, const char* szFriendlyName, const char* szVerIndProgID, const char* szProgID, const char* szThreadingModel); // This function will unregister a component. HRESULT UnregisterServer(REFCLSID clsid, const char* szVerIndProgID, const char* szProgID); struct REG_DATA { const char* pszKey; const char* pszValue; const char* pszData; }; // These functions will register and unregister a component // based on data from a global array. HRESULT RegisterServerEx(const REG_DATA regData[], const char* szModuleName); HRESULT UnregisterServerEx(const REG_DATA regData[]); |
Listing 2-6. The registry.h file.
Several steps are required to replace the registration file used to register the component in the previous section. First, you must include the registry.h file containing forward declarations for the registration functions, and then you must write the DllRegisterServer and DllUnregisterServer functions, as shown here:
// component.cpp #include "registry.h" // Add this!!! // And these... HRESULT __stdcall DllRegisterServer() { return RegisterServer("component with registration.dll", CLSID_InsideCOM, "Inside COM+: In Process Component", "Component.InsideCOM", "Component.InsideCOM.1", NULL); } HRESULT __stdcall DllUnregisterServer() { return UnregisterServer(CLSID_InsideCOM, "Component.InsideCOM" "Component.InsideCOM.1"); } |
Notice that the RegisterServer and UnregisterServer functions both require certain pieces of information that will be entered into the registry. While this makes it easy to understand how they work, it also limits the flexibility of the registration code, because every new entry not supported by these functions must be added manually or else you must modify the implementation of the registry.cpp code. A newer and more flexible way to add registry entries is to define an array of registry data in the application and have the registration code insert this information into the registry for you.14 So as an alternative to the RegisterServer and UnregisterServer functions, we have also written RegisterServerEx and UnregisterServerEx, which use this new method based on an array. The array containing the registry data is shown below:
const REG_DATA g_regData[] = { { "CLSID\\{10000002-0000-0000-0000-000000000001}", 0, "Inside COM+: In Process Component" }, { "CLSID\\{10000002-0000-0000-0000-000000000001}" "\\InprocServer32", 0, (const char*)-1 }, { "CLSID\\{10000002-0000-0000-0000-000000000001}\\ProgID", 0, "Component.InsideCOM.1" }, { "CLSID\\{10000002-0000-0000-0000-000000000001}" "\\VersionIndependentProgID", 0, "Component.InsideCOM" }, { "Component.InsideCOM", 0, "Inside COM+: In Process Component" }, { "Component.InsideCOM\\CLSID", 0, "{10000002-0000-0000-0000-000000000001}" }, { "Component.InsideCOM\\CurVer", 0, "Component.InsideCOM.1" }, { "Component.InsideCOM.1", 0, "Inside COM+: In Process Component" }, { "Component.InsideCOM.1\\CLSID", 0, "{10000002-0000-0000-0000-000000000001}" }, { 0, 0, 0 } }; |
Although this array looks a bit messy, it is more flexible because any registry settings that need to be added require only another entry in the array—you don't have to modify the actual registration functions. The array begins with the subkey name of HKEY_CLASSES_ROOT, followed by a named value (if any) and the actual data. If the named value is 0, the data element is assigned to the default value. A named value of _1 is a placeholder indicating that the fully qualified pathname of the component should be inserted. This is retrieved via a call to the Win32 API function GetModuleFileName. The last entry (0, 0, 0) indicates the end of the array.
Now the DllRegisterServer and DllUnregisterServer functions can be simplified, as shown below:
HINSTANCE g_hInstance; // Global hInstance of the DLL HRESULT __stdcall DllRegisterServer() { char DllPath[MAX_PATH]; GetModuleFileName(g_hInstance, DllPath, sizeof(DllPath)); return RegisterServerEx(g_regData, DllPath); } HRESULT __stdcall DllUnregisterServer() { return UnregisterServerEx(g_regData); } |
The GetModuleFileName function, which retrieves the path and filename of the DLL, requires the instance handle (hInstance) of the DLL. This handle is passed to the DllMain function and is stored in the g_hInstance global variable when the DLL is loaded, as shown here:
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, void* pv) { g_hInstance = hInstance; return TRUE; } |
Regardless of which registration method you use, you must add the DllRegisterServer and DllUnregisterServer functions to the end of the exported function list in the module definition file, as shown in boldface in Listing 2-7:
component with registration.def
LIBRARY component.dll DESCRIPTION '(c)1999 Guy Eddon' EXPORTS DllGetClassObject @1 PRIVATE DllCanUnloadNow @2 PRIVATE DllRegisterServer @3 PRIVATE DllUnregisterServer @4 PRIVATE |
Listing 2-7. The component with the registration.def file, which contains the exported functions DllRegisterServer and DllUnregisterServer.
The OLESelfRegister flag Some developers consider the OLESelfRegister flag an anachronism in COM+, and don't bother including it in a component's version information resource. This is regrettable because the flag serves an important purpose: it tells a client whether a component supports self-registration. Without it, a potential client must load a component and attempt to activate its self-registration code. This is not such a problem for in-process components because a client can simply check for the existence of the DllRegisterServer function. If it exists in the DLL's export table, the component almost definitely supports self-registration. Still, the DLL must be loaded to make the determination, which can waste resources, unless the client wants to use knowledge of the Win32 DLL file format to check the export table for the DllRegisterServer function; this latter method is certainly not recommended. For executable components, the situation gets even worse: there is simply no way to determine whether an executable component supports self-registration. A client can only launch the executable with the /RegServer command-line parameter and hope for the best. If self-registration is not supported, it is impossible to tell what the executable component might do.
One example of a service that uses the OLESelfRegister flag is Internet Component Download. Microsoft Internet Explorer uses this service to automatically download components such as ActiveX controls from a Web site. For security reasons, the .inf format used by the Internet Component Download service does not include syntax for changing registry information. Instead, the service looks inside .ocx, .dll, and .exe files for the OLESelfRegister flag in the version information resource. For components that are marked as self-registering, the service attempts to activate the self-registration code in the component. Components that are not marked in this way are not automatically registered. This setting can be overridden in the .inf file.
When you create a self-registering component, you should always indicate this feature by including the OLESelfRegister flag in the version information section of the resource script file. Visual Basic, for example, always includes the OLESelfRegister flag in all components built in that language. (Of course, Visual Basic also provides the self-registration code.) The OLESelfRegister flag is shown in boldface in Listing 2-8. A client program can read the version information resource out of an .ocx, .dll, or .exe file using the file installation library functions of the Win32 API.15
component.rc
VS_VERSION_INFO VERSIONINFO FILEVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L #else FILEFLAGS 0x0L #endif FILEOS 0x10004L FILETYPE 0x1L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", "Microsoft Corporation\0" VALUE "FileDescription", "Inside COM+ Component\0" VALUE "FileVersion", "1, 0, 0, 1\0" VALUE "InternalName", "InsideCOM+\0" VALUE "LegalCopyright", "Copyright © 1999\0" VALUE "OriginalFilename", "component.dll\0" VALUE "ProductName", "Inside COM+\0" VALUE "ProductVersion", "2, 0, 0, 1\0" VALUE "OLESelfRegister", "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x0409, 1200 END END |
Listing 2-8. A resource file that you can use to determine whether a component supports self-registration.
To build the component that supports self-registration, open the InProcess.dsw workspace file in the Samples\The IUnknown Interface folder and build the Component with Registration project. Then use RegSvr32 to test the new functionality. You should see the message box shown in Figure 2-7.
Figure 2-7. The message box that appears when you register an in-process component using RegSvr32.exe.