| < Free Open Study > |
|
Now that we have a better understanding of ATL's threading support, we are in a good position to examine the architecture of a typical ATL coclass. For the purpose of this discussion, assume we have created an in-process server named ATLShapesServer.dll, which contains a single ATL Simple Object named CoRectangle. The name of the [default] custom interface is IDraw.
As we drill into the anatomy of CoRectangle, keep in mind that ATL was designed to produce the smallest and fastest COM objects around. To achieve this goal, ATL uses some rather bizarre coding conventions, which introduce a number of surprises to the uninitiated developer. Here is the first such surprise: CoRectangle is an abstract base class at this point and cannot be directly created. Recall that abstract base classes contain at least one pure virtual function. Therefore the following will not compile:
// Can't create abstract base classes! CCoRectangle *pRect = new CCoRectangle(); // Compiler error.
Truth be told, CoRectangle does not yet have an implementation of the IUnknown interface. As you may have guessed (or are already aware) CoRectangle is not the most derived class in the ATL inheritance chain. ATL objects are in fact passed as a parameter to one of the various CComObject<> templates. These ATL templates are responsible for providing a specific implementation of IUnknown for your coclass.
So, why does the ATL framework choose to specify the implementation of IUnknown "one level below" your coclass? Independence. COM objects may be created under a number of circumstances: on the stack, on the heap, as part of an aggregate, as a tear-off interface, and so forth (if some of these terms are unfamiliar to you at this point, keep reading). Each of these variations will require a slightly different implementation of the IUnknown interface.
Rather than hard coding a specific IUnknown implementation directly into your coclass (such as CoRectangle), ATL provides a number of variations of CComObject<> templates which are used to assemble your coclass's IUnknown implementation under a number of different circumstances, keeping your ATL source code far more portable. For example, CoRectangle can be created as an object that does influence the server's object count or as an object that does not alter the server's object count, without altering the code base in CoRectangle. Instead, simply pass the ATL coclass into the correct variation of the CComObject<> template:
// Create a CoRectangle which keeps the server in memory. CComObject< CCoRectangle >*pRect = NULL; CComObject< CCoRectangle >::CreateInstance(&pRect); // Create a CoRectangle which does not keep the server in memory. CComObjectNoLock< CCoRectangle>*pRect = new CComObjectNoLock <CCoRectangle>;
Recall from Chapter 5 that class factories dwelling in EXEs should not alter the server- wide object count. Class factories in DLLs must adjust the server object count. This forced us to create two versions of the CoCar class factory, which differed only in the way they adjusted the server's global object counter. ATL abstracts away these minor differences by resolving the implementation of such details outside of the coclass itself.
The most common CComObject<> template is CComObject<> itself, which creates a heap-based, non-aggregated object. When passing an ATL coclass into CComObject<>, you should call the static CreateInstance() method, which ensures the framework triggers FinalConstruct() (we will see this shortly). If you do use the new keyword directly (as seen above with CComObjectNoLock<>), FinalConstruct() is not called, although you still do have a usable ATL COM object. Some of the most common variations of CComObject<> are seen below and are formally found in <atlcom.h>:
CComObject<> Template | Meaning in Life |
---|---|
CComObject<> | Creates a non-aggregated, heap-based object which affects the server's object count (m_nLockCnt). |
CComObjectNoLock<> | Creates non-aggregated objects that do not adjust the server's object count (m_nLockCnt). |
CComAggObject<> | Used to create classes that can work as an aggregate. |
CComObjectStack<> | Creates objects whose lifetime is limited to some function scope. |
CComObjectGlobal<> | Creates objects whose lifetime is dependent upon the lifetime of the server. Thus, the object's reference counter is based off CComModule::m_nLockCnt. |
CComPolyObject<> | Creates objects that may or may not work as an aggregate. |
CComTearOffObject<> | Creates an object that implements a tear-off interface. |
Most of the time, you will not need to make use of these ATL templates directly, as the framework will use the correct variation when assembling your objects. In Chapter 8, we will begin to examine the full set of ATL's COM map macros. As you will see, many of these macros (when expanded) make use of these CComObject<> alternatives on your behalf.
For example, if you are exposing an interface as a tear-off, your COM map will have a listing for COM_INTERFACE_ENTRY_TEAR_OFF, which makes use of CComTearOffObject<>:
// Many ATL COM map macros used CComObject<> templates // on your behalf. #define COM_INTERFACE_ENTRY_TEAR_OFF(iid, x)\ {&iid,\ (DWORD)&_CComCreatorData<\ CComInternalCreator< CComTearOffObject< x > >\ >::data,\ _Creator},
If you ever need to create an instance of an ATL coclass from within an ATL coclass (i.e., create an ATL object defined within your project) you would need to do so by hand and pass the coclass in question into one of the ATL CComObject<> templates. For the time being, we will only concern ourselves with CComObject<> and wait until Chapter 8 to examine the various alternatives.
In addition to the support provided by the CComObject<> templates, CoRectangle receives a good deal of base class functionality. Figure 7-11 illustrates the derivation for CoRectangle. We will examine each level in turn, so just attempt to absorb the big picture for now:
Figure 7-11: The derivation of an ATL Simple Object.
First off, each ATL coclass will derive from the set of interfaces that it supports. In the case of CoRectangle, this is IDraw (which in turn derives from IUnknown).
Beyond this, ATL Simple Objects derive from three framework base classes: CComObjectRootBase, CComObjectRootEx<>, and CComCoClass<>. Here is the initial code listing for CoRectangle:
// The CoRectangle Simple Object. class ATL_NO_VTABLE CCoRectangle : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoRectangle, &CLSID_CoRectangle>, public IDraw { public: CCoRectangle() {} // We will examine this macro in Chapter 9... DECLARE_REGISTRY_RESOURCEID(IDR_CORECTANGLE) BEGIN_COM_MAP(CCoRectangle COM_INTERFACE_ENTRY(IDraw) END_COM_MAP() };
To better understand how ATL assembles a coclass, let's drill into the functionality provided by the framework from the top down, beginning with the ATL_NO_VTABLE class tag. If COM is all about handing out vTable pointers to interested clients, what could this possibly mean? By default, ATL classes are declared using ATL_NO_VTABLE, which is defined as the following:
// This class tag is nothing more than an optimization. #ifdef _ATL_DISABLE_NO_VTABLE #define ATL_NO_VTABLE #else #define ATL_NO_VTABLE __declspec(novtable) #endif
Note | If your project defines the _ATL_DISABLE_NO_VTABLE symbol (which you may do from the Project Settings dialog), _ATL_NO_VTABLE resolves to nothing, effectively disabling this option for every ATL coclass in the project. |
This compiler optimization prevents the vPtr for a class from being initialized in the constructor and destructor of the class. Of course, after the constructor call, you do have a valid vPtr at your disposal. Why might we want to do this? Well, initializing a vPtr takes time. Technically speaking, the only time a derived class needs a proper vPtr is when it is the most derived class in the hierarchy. We have just seen that ATL objects are not the most derived, but are passed as a template parameter to CComObject<> and friends. ATL is doing everything it can to create zippy, lightweight COM objects, and this is just one example of how it is doing so.
There is one point you must be aware of when using the ATL_NO_VTABLE optimization. As you have no vPtr to work with in your constructor and destructor blocks, you cannot safely call any virtual or pure virtual methods of your base classes in their implementations! How then can you leverage base class functionality? Glad you asked.
CComObjectRootBase defines two methods, which allow your class to safely perform any instance level initialization and termination code, as well as access base class functionality. Rather than accessing base class functionality in your constructor, use FinalConstruct(). If you need to perform any cleanup, use FinalRelease() rather than the destructor. These methods are given a default implementation in CComObjectRootBase as follows:
// Secondary creation call. HRESULT FinalConstruct() { return S_OK; } // Pre-destruction call. void FinalRelease() { }
As you can see, these methods are implemented as simple stubs. Your derived class (such as CoRectangle) can override these methods in order to do any post-constructor and/or pre-destructor calls. FinalConstruct() will be called by the ATL framework right after the call to your coclass constructor. In this method you may safely access any base class functionality and create any inner objects (which we will do later), as well as initialize your own internal state data.
FinalRelease() will be called just before your object is about to be destroyed, and allows you to perform any cleanup required by the class. Using FinalConstruct() and FinalRelease() in your derived coclass is trivial:
// Overriding FinalConstruct() and FinalRelease(). // class ATL_NO_VTABLE CCoRectangle : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoRectangle, &CLSID_CoRectangle>, public IDraw { ... // Safe to access parents. HRESULT FinalConstruct() { MessageBox(NULL, "I have arrived!", "CoRect!", MB_OK | MB_SETFOREGROUND); return S_OK; } // Clean up after CoRectangle. void FinalRelease() { MessageBox(NULL, "I'm outta here", " CoRect!", MB_OK | MB_SETFOREGROUND); } ... };
Now that the ATL_NO_VTABLE tag has been demystified, the next order of business is to examine CComObjectRootBase in some greater detail.
Although you do not see mention of this class directly in your inheritance chain, CComObjectRootBase (defined in <atlcom.h>) is at the very top of an ATL coclass hierarchy. Beyond defining FinalConstruct() and FinalRelease(), CComObjectRootBase maintains a reference counting variable (m_dwRef) for an ATL coclass. This public data member is initialized to zero in the constructor of the class:
// m_dwRef is your object's reference counter. That is why we do not need to provide // a private reference counter in our ATL coclasses. The framework maintains this on // your behalf. CComObjectRootBase() { m_dwRef = 0L; }
In addition to specifying the m_dwRef data member to represent your object's reference count, CComObjectRootBase also defines a method named InternalQueryInterface() which is used to obtain interface pointers for non-aggregated COM objects.
When your COM object is passed into the CComObject<> template, the "real" implementation of IUnknown::QueryInterface() will eventually call InternalQueryInterface(). This method in turn calls a helper method which performs the grunt work to see if a given interface is supported by your coclass:
// InternalQueryInterface() is used to discover if your coclass supports a given interface. class CComObjectRootBase { public: ... static HRESULT WINAPI InternalQueryInterface(void* pThis, const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) { ... HRESULT hRes = AtlInternalQueryInterface(pThis, pEntries, iid, ppvObject); ... } };
InternalQueryInterface() takes a number of parameters: a pointer to the class itself (*pThis), the class's COM map (*pEntries), the GUID of the interface to be found (iid), and a place to store the interface pointer (**ppvObject). As we will see a bit later in this chapter, the ATL COM map defines an array of _ATL_INTMAP_ENTRY structures. Each structure in this array contains vital information about how to calculate the vPtr for a given interface supported by the coclass.
InternalQueryInterface() delegates the bulk of the work to a helper function named AtlInternalQueryInterface(). This method iterates over the COM map, testing for a given IID, until a NULL entry is discovered which signifies the end of the COM map.
We will see the code behind AtlInternalQueryInterface() a bit later in this chapter. Do understand, however, that this is the method responsible for obtaining interface pointers for any interested clients based on your coclass's COM map.
CComObjectRootBase also defines methods used for tear-off interfaces and COM aggregation. ATL provides a set of "outer" IUnknown methods for this purpose, as well as a public IUnknown pointer (m_pOuterUnknown) used by the aggregated class. We will discuss aggregation and tear-off interfaces in Chapter 8, but since we are examining CComObjectRootBase at the moment, here are the outer IUnknown members:
// The 'outer' methods are used only for aggregation and tear-off relationships. class CComObjectRootBase { public: ... ULONG OuterAddRef() { return m_pOuterUnknown->AddRef(); } ULONG OuterRelease() { return m_pOuterUnknown->Release(); } HRESULT OuterQueryInterface(REFIID iid, void ** ppvObject) { return m_pOuterUnknown->QueryInterface(iid, ppvObject); } // Pointer to the aggregating object's IUnknown. IUnknown* m_pOuterUnknown; ... };
Finally, CComObjectRootBase defines six internal helper functions (_Chain(), _Break(), _NoInterface(), _Cache(), _Delegate(), and _Creator()) that are used by various ATL COM map macros to help calculate the vPtr for a given interface which is not supported using multiple inheritance. Here is a breakdown of when each of these internal helper methods are called:
CComObjectRootBase Helper Method | Meaning in Life |
---|---|
_Break() | Called internally by COM_INTERFACE_ENTRY_BREAK to issue a debug break. |
_NoInterface() | Called internally by COM_INTERFACE_ENTRY_NOINTER- FACE. Automatically returns E_NOINTERFACE for a requested interface. |
_Creator() | Called internally by COM_INTERFACE_ENTRY_TEAR_OFF to create an instance of the tear-off class. |
_Delegate() | Called internally by the non-auto aggregation macros COM_INTERFACE_ENTRY_AGGREGATE and COM_INTER- FACE_ENTRY_AGGREGATE_BLIND to query an aggregated object for a given interface. |
_Chain() | Called internally by COM_INTERFACE_ENTRY_CHAIN to forward a QueryInterface() call to a base class COM map. |
_Cache() | Called by the ATL auto-aggregation macros as well as by COM_INTERFACE_ENTRY_CACHED_TEAR_OFF to create a cached tear-off class. |
Much like the alternative CComObject<> templates, you should not need to interact with these helper functions directly. The framework will call these methods where appropriate. Take, for example, the COM_INTERFACE_ENTRY_NOINTERFACE macro. When expanded, a call to CComObjectRootBase::_NoInterface() is made automatically:
// Many of the more exotic COM map macros call these CComObjectRootBase helper // functions to calculate a vPtr for an interface (or not). #define COM_INTERFACE_ENTRY_NOINTERFACE(x)\ {&_ATL_IIDOF(x), \ NULL, \ _NoInterface},
The implementation of _NoInterface() does what you might expect:
// COM_INTERFACE_ENTRY_NOINTERFACE calls _NoInterface() to explicitly // inform a client that the coclass does not support the interface in question. static HRESULT WINAPI _NoInterface(void* /* pv */, REFIID /* iid */, void** /* ppvObject */, DWORD /* dw */) { return E_NOINTERFACE; }
Each of these internal helper functions, and the macros that call them, are found in <atlcom.h>. Again, we will detail these alternative COM map macros and helper functions in Chapter 8.
So much for CComObjectRootBase. Let's take a look at the first direct parent to CoRectangle, CComObjectRootEx<>:
// The sole parameter to CComObjectRootEx<> specifies a threading model for the // coclass. class ATL_NO_VTABLE CCoRectangle : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoRectangle, &CLSID_CoRectangle>, public IDraw { ... };
As we have seen earlier in this chapter, the parameter to CComObjectRootEx<> specifies the level of threading support for a given coclass using CComSingleThreadModel (STA) or CComMultiThreadModel (MTA).
CComObjectRootEx<> is a very lightweight class, providing exactly four methods and a critical section typedef. This typedef is assembled based on the threading class passed in as a template parameter. The first two methods, Lock() and Unlock(), are used in your coclass to enter or leave a critical section (which as you recall is a "fake" critical section for objects in the STA).
The next two methods, InternalAddRef() and InternalRelease(), are used to adjust the reference counter of non-aggregated objects and to define how the coclass should implement reference counting with regard to the appropriate level of thread safety.
Here is the definition of CComObjectRootEx<> as defined in <atlcom.h>:
// Your object's reference counting scheme will be "thread safe enough." template <class ThreadModel> class CComObjectRootEx : public CComObjectRootBase { public: // Threading model typedefs. typedef ThreadModel _ThreadModel; typedef _ThreadModel::AutoCriticalSection _CritSec; ... // ++ or InterlockedIncrement() ULONG InternalAddRef() { return _ThreadModel::Increment(&m_dwRef); } // -- or InterlockedDecrement() ULONG InternalRelease() { return _ThreadModel::Decrement(&m_dwRef); } // Do nothing or enter a critical section. void Lock() { m_critsec.Lock(); } // Do nothing or leave a critical section. void Unlock() { m_critsec.Unlock(); } private: // CComFakeCriticalSection or CComAutoCriticalSection. _CritSec m_critsec; };
The final base class, CComCoClass<>, is found towards the end of CoRectangle's inheritance chain:
// The final core ATL base class. class ATL_NO_VTABLE CCoRectangle : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoRectangle, &CLSID_CoRectangle>, public IDraw { ... };
This class is used to establish two critical aspects for your ATL object. First of all, CComCoClass<> defines the default level of aggregation support for the coclass. Next, CComCoClass<> supplies a default class factory used to specify how to create the CoRectangle. Both of these details are defined using ATL-specific macros:
// CoRectangle's aggregation support and class object are defined using ATL macros // within the CComCoClass template. template <class T, const CLSID* pclsid = &CLSID_NULL> class CComCoClass { ... public: // Provides a generic class factory. DECLARE_CLASSFACTORY() // Sets up default aggregation support. DECLARE_AGGREGATABLE(T) ... };
The DECLARE_CLASSFACTORY macro expands to define the ATL class implementing IClassFactory. By default, this class is CComClassFactory. We will have much to say about CComClassFactory, as well as how to override this default behavior in Chapter 9. Just realize at this point that the reason ATL does not require you to create a class factory by hand is that the DECLARE_CLASSFACTORY macro gives you a generic class factory for free.
CComCoClass also defines the DECLARE_AGGREGATABLE macro to instruct the ATL framework how CoRectangle should respond if asked to become aggregated by another object. The DECLARE_AGGREGATABLE macro allows CoRectangle to function correctly as a stand-alone object or as an aggregated object. We will examine aggregation in greater detail in Chapter 8.
Beyond establishing a class object and aggregation support, CComCoClass specifies a number of overloaded Error() methods dealing with COM exceptions. Your ATL coclasses may make use of these methods to return rich error information to a COM client beyond the standard HRESULT. We will see how to do so later in this chapter.
So far we have seen the functionality given to CoRectangle from three core base classes:
CComObjectRootBase provides a number of methods to support COM aggregation and interface resolution and defines the object's internal reference counter (m_dwRef).
CComObjectRootEx<> supports a "thread safe enough" reference counting scheme, as well as providing Lock() and Unlock() methods.
CComCoClass<> provides a class factory for the object, as well as sets up the object's level of "aggregation awareness" and the ability to throw COM exceptions.
At this point, CoRectangle will be passed into the correct CComObject<> template, which provides a specific implementation of IUnknown. The constructor of CComObject<> increases the global object counter variable (m_nLockCnt), while the destructor calls FinalRelease() and decrements the same variable. This logic ensures your server remains loaded as long as there is an instance of CoRectangle in use:
// Recall the name of your CComModule instance is _Module. template <class Base> class CComObject : public Base { public: ... // Increment m_nLockCnt. CComObject(void* = NULL) { _Module.Lock(); } // Call FinalRelease() and decrement m_nLockCnt. ~CComObject() { FinalRelease(); _Module.Unlock(); } ... };
CComObject<> is responsible for providing the actual implementation of your coclass's IUnknown interface. Here at long last we see the markings of a true COM object: QueryInterface(), AddRef(), and Release(). Notice how the implementation of each method makes calls on the "internal" IUnknown methods defined by CComObjectRootBase and CComObjectRootEx<>:
// CComObject<> creates non-aggregated, heap-based objects. template <class Base> class CComObject : public Base { public: ... // InternalAddRef() is defined in CComObjectRootEx<> STDMETHOD_(ULONG, AddRef)() { return InternalAddRef(); } // InternalRelease() is also defined in CComObjectRootEx<> STDMETHOD_(ULONG, Release)() { ULONG l = InternalRelease(); if (l == 0) delete this; return l; } // _InternalQueryInterface() is a helper function // defined by your COM map, which in turn calls // CComObjectRootBase::InternalQueryInterface() STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) { return _InternalQueryInterface(iid, ppvObject); } };
Another important aspect of CComObject<> is the static CreateInstance() method. When you need to create an ATL COM object from within your ATL project (meaning both objects are in the same binary server) you should call CreateInstance() to allocate your object and ensure a call to FinalConstruct(). For example:
// Create an ATL COM object from within an ATL project. CComObject<CCoRectangle> *pRect; CComObject<CCoRectangle>::CreateInstance(&pRect);
Here is the implementation of CComObject::CreateInstance():
// CreateInstance() ensures FinalConstruct() is called. template <class Base> HRESULT WINAPI CComObject<Base>::CreateInstance(CComObject<Base>** pp) { ATLASSERT(pp != NULL); HRESULT hRes = E_OUTOFMEMORY; CComObject<Base>* p = NULL; ATLTRY(p = new CComObject<Base>()) if (p != NULL) { ... hRes = p->FinalConstruct(); ... if (hRes != S_OK) { delete p; p = NULL; } } *pp = p; return hRes; }
That brings us to the end of the CoRectangle hierarchy! While having a better understanding of what ATL is doing to represent your COM objects is certainly helpful, you have to agree the road is long and winding.
In a nutshell, the ATL base classes work together to provide your coclass with the correct level of thread safety, a default class factory, aggregation support, and a full implementation of IUnknown. Now that we have peeked into the framework support for CoRectangle, let's turn our attention to the details of the ATL COM_MAP structure.
| < Free Open Study > |
|