CComObject Et
Al
Consider the following C++ class:
class CPenguin :
public CComObjectRootEx<CComMultiThreadModel>,
public IBird,
public ISnappyDresser {
public:
BEGIN_COM_MAP(CPenguin)
COM_INTERFACE_ENTRY(IBird)
COM_INTERFACE_ENTRY(ISnappyDresser)
END_COM_MAP()
// IBird and ISnappyDresser methods...
// IUnknown methods not implemented here
};
Because this class doesn't implement the methods
of IUnknown, the following will fail at compile time:
STDMETHODIMP
CPenguinCO::CreateInstance(IUnknown* pUnkOuter,
REFIID riid, void** ppv) {
...
CPenguin* pobj = new CPenguin; // IUnknown not implemented
...
}
Given
CComObjectRootBase, you can easily implement the methods
of IUnknown:
// Server lifetime management
extern void ServerLock();
extern void ServerUnlock();
class CPenguin :
public CComObjectRootEx<CComMultiThreadModel>,
public IBird,
public ISnappyDresser {
public:
CPengin() { ServerLock(); }
~CPenguin() { ServerUnlock(); }
BEGIN_COM_MAP(CPenguin)
COM_INTERFACE_ENTRY(IBird)
COM_INTERFACE_ENTRY(ISnappyDresser)
END_COM_MAP()
// IBird and ISnappyDresser methods...
// IUnknown methods for standalone, heap-based objects
STDMETHODIMP QueryInterface(REFIID riid, void** ppv)
{ return _InternalQueryInterface(riid, ppv); }
STDMETHODIMP_(ULONG) AddRef()
{ return InternalAddRef(); }
STDMETHODIMP_(ULONG) Release() {
ULONG l = InternalRelease();
if( l == 0 ) delete this;
return l;
}
};
Unfortunately, although this implementation does
leverage the base class behavior, it has hard-coded assumptions
about the lifetime and identity of our objects. For example,
instances of this class can't be created as an aggregate. Just as
we're able to encapsulate decisions about thread safety into the
base class, we would like to encapsulate
decisions about lifetime and identity. However, unlike
thread-safety decisions, which are made on a per-class basis and
are, therefore, safe to encode into a base class, lifetime and
identity decisions can be made on a per-instance basis. Therefore,
we'll want to encapsulate lifetime and identity behavior into
classes meant to derive from our class.
Standalone
Activation
To encapsulate the standalone, heap-based object
implementation of IUnknown I just showed you, ATL provides
CComObject, shown in a slightly abbreviated form here:
template <class Base>
class CComObject : public Base {
public:
typedef Base _BaseClass;
CComObject(void* = NULL)
{ _pAtlModule->Lock(); } // Keeps server loaded
// Set refcount to -(LONG_MAX/2) to protect destruction and
// also catch mismatched Release in debug builds
~CComObject() {
m_dwRef = -(LONG_MAX/2);
FinalRelease();
#ifdef _ATL_DEBUG_INTERFACES
_AtlDebugInterfacesModule.DeleteNonAddRefThunk(
_GetRawUnknown());
#endif
_pAtlModule->Unlock(); // Allows server to unload
}
STDMETHOD_(ULONG, AddRef)() {return InternalAddRef();}
STDMETHOD_(ULONG, Release)() {
ULONG l = InternalRelease();
if (l == 0) delete this;
return l;
}
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{return _InternalQueryInterface(iid, ppvObject);}
template <class Q>
HRESULT STDMETHODCALLTYPE QueryInterface(Q** pp)
{ return QueryInterface(__uuidof(Q), (void**)pp); }
static HRESULT WINAPI CreateInstance(CComObject<Base>** pp) ;
};
Notice that CComObject takes a template
parameter called Base. This is the base class from which
CComObject derives to obtain the functionality of
CComObjectRootEx, as well as whatever custom functionality
we'd like to include in our objects. Given the implementation of
CPenguin that did not include the implementation of the
IUnknown methods, the compiler would be happy with
CComObject used as follows (although I describe later why
new shouldn't be used directly when creating ATL-based COM
objects):
STDMETHODIMP
CPenguinCO::CreateInstance(IUnknown* pUnkOuter, REFIID riid,
void** ppv) {
*ppv = 0;
if( pUnkOuter ) return CLASS_E_NOAGGREGATION;
// Read on for why not to use new like this!
CComObject<CPenguin>* pobj = new CComObject<CPenguin>;
if( pobj ) {
pobj->AddRef();
HRESULT hr = pobj->QueryInterface(riid, ppv);
pobj->Release();
return hr;
}
return E_OUTOFMEMORY;
}
Besides the call to FinalRelease and
the static member function CreateInstance (which are both
described in the "Creators" section of this
chapter), CComObject provides one additional item of note,
the QueryInterface member function template:
template <class Q>
HRESULT STDMETHODCALLTYPE QueryInterface(Q** pp)
{ return QueryInterface(__uuidof(Q), (void**)pp); }
This member function template uses the
capability of the VC++ compiler to tag a type with a universally
unique identifier (UUID). This capability has been available since
VC++ 5.0 and takes the form of a declarative specifier
(declspecs):
struct __declspec(uuid("00000000-0000-0000-C000-000000000046") IUnknown
{...};
These declspec
specifiers are output by the Microsoft IDL compiler and are
available for both standard and custom interfaces. You can retrieve
the UUID of a type using the __uuidof operator, allowing
the following syntax:
void TryToFly(IUnknown* punk) {
IBird* pbird = 0;
if( SUCCEEDED(punk->QueryInterface(__uuidof(pbird),
(void**)&pbird) ) {
pbird->Fly();
pbird->Release();
}
}
Using the QueryInterface member
function template provided in CComObject offers a bit more
syntactic convenience, given a CComObject-based object
reference:
void TryToFly(CComObject<CPenguin>* pPenguin) {
IBird* pbird = 0;
if( SUCCEEDED(pPenguin->QueryInterface(&pbird) ) {
pbird->Fly();
pbird->Release();
}
}
Aggregated
Activation
Notice that the CPenguin class object
implementation shown previously disallowed aggregation by checking
for a nonzero pUnkOuter and returning
CLASS_E_NOAGGREGATION. If we want to support aggregation
as well asor instead ofstandalone activation, we need another class
to implement the forwarding behavior of aggregated instances. For
this, ATL provides CComAggObject.
CComAggObject performs the chief
service of being a controlled innerthat is, providing two
implementations of IUnknown. One implementation forwards
calls to the controlling outer, subsumed by its lifetime and
identity. The other implementation is for private use of the
controlling outer for actually maintaining the lifetime of and
querying interfaces from the inner. To obtain the two
implementations of IUnknown, CComAggObject
derives from CComObjectRootEx twice, once directly and
once indirectly via a contained instance of your class derived from
CComContained-Object, as shown here:
template <class contained>
class CComAggObject :
public IUnknown,
public CComObjectRootEx<
contained::_ThreadModel::ThreadModelNoCS> {
public:
typedef contained _BaseClass;
CComAggObject(void* pv) : m_contained(pv)
{ _pAtlModule->Lock(); }
~CComAggObject() {
m_dwRef = -(LONG_MAX/2);
FinalRelease();
_pAtlModule->Unlock();
}
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {
ATLASSERT(ppvObject != NULL);
if (ppvObject == NULL)
return E_POINTER;
*ppvObject = NULL;
HRESULT hRes = S_OK;
if (InlineIsEqualUnknown(iid)) {
*ppvObject = (void*)(IUnknown*)this;
AddRef();
}
else
hRes = m_contained._InternalQueryInterface(iid,
ppvObject);
return hRes;
}
STDMETHOD_(ULONG, AddRef)()
{ return InternalAddRef(); }
STDMETHOD_(ULONG, Release)() {
ULONG l = InternalRelease();
if (l == 0) delete this;
return l;
}
template <class Q>
HRESULT STDMETHODCALLTYPE QueryInterface(Q** pp)
{ return QueryInterface(__uuidof(Q), (void**)pp); }
static HRESULT WINAPI CreateInstance(LPUNKNOWN pUnkOuter,
CComAggObject<contained>** pp);
CComContainedObject<contained> m_contained;
};
You can see that instead of deriving from your
class (passed as the template argument), CComAggObject
derives directly from CComObjectRootEx. Its implementation
of QueryInterface relies on the interface map you've built
in your class, but its implementation of AddRef and
Release access relies on the second instance of
CComObjectRootBase it gets by deriving from
CComObjectRootEx. This second instance of
CComObjectRootBase uses the m_dwRef member of the
union.
The first instance of
CComObjectRootBase, the one that manages the
m_p-OuterUnknown member of the union, is the one
CComAggObject gets by creating an instance of your class
derived from CComContainedObject as the
m_contained data member. CComContainedObject
implements QueryInterface, AddRef, and
Release by delegating to the m_pOuterUnknown
passed to the constructor:
template <class Base>
class CComContainedObject : public Base {
public:
typedef Base _BaseClass;
CComContainedObject(void* pv) {
m_pOuterUnknown = (IUnknown*)pv;
}
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {
return OuterQueryInterface(iid, ppvObject);
}
STDMETHOD_(ULONG, AddRef)()
{ return OuterAddRef(); }
STDMETHOD_(ULONG, Release)()
{ return OuterRelease(); }
template <class Q>
HRESULT STDMETHODCALLTYPE QueryInterface(Q** pp)
{ return QueryInterface(__uuidof(Q), (void**)pp); }
IUnknown* GetControllingUnknown()
{ return m_pOuterUnknown; }
};
Being the
Controlled Inner
Using CComAggObject and its two
implementations of IUnknown, our CPenguin class
object implementation can support either standalone or aggregated
activation without touching the CPenguin source:
STDMETHODIMP
CPenguinCO::CreateInstance(IUnknown* pUnkOuter, REFIID riid,
void** ppv) {
*ppv = 0;
if( pUnkOuter ) {
CComAggObject<CPenguin>* pobj =
new CComAggObject<CPenguin>(pUnkOuter);
...
}
else {
CComObject<CPenguin>* pobj = new CComObject<CPenguin>;
...
}
}
This usage provides the most efficient runtime
decision making. If the object is standalone, it pays the price of
one reference count and one implementation of IUnknown. If
it is aggregated, it pays the price of one reference count, one
pointer to the controlling outer, and two implementations of
IUnknown. However, one additional price we're paying is
one extra set of vtbls. By using both
CComAggObject<CPenguin> and
CComObject<CPenguin>, we've created two classes and,
therefore, two sets of vtbls. If you've got a small number
of instances or nearly all your instances are aggregated, you might
want a single class that can handle both aggregated and standalone
activation, thereby eliminating one set of vtbls. You do
this by using CComPolyObject in place of both
CComObject and CComAggObject:
STDMETHODIMP
CPenguinCO::CreateInstance(IUnknown* pUnkOuter, REFIID riid,
void** ppv) {
*ppv = 0;
CComPolyObject<CPenguin>* pobj =
new CComPolyObject<CPenguin>(pUnkOuter);
...
}
CComPolyObject is nearly identical to
CComAggObject, except that, in its constructor, if the
pUnkOuter is zero, it uses its second implementation of
IUnknown as the outer for the first to forward to, as
shown:
class CComPolyObject :
public IUnknown,
public CComObjectRootEx<
contained::_ThreadModel::ThreadModelNoCS> {
public:
...
CComPolyObject(void* pv) : m_contained(pv ? pv : this) {...}
...
};
The use of
CComPolyObject saves a set of vtbls, so the
module size is smaller, but the price you pay for standalone
objects is getting an extra implementation of IUnknown as
well as an extra pointer to that implementation.
Alternative
Activation Techniques
Besides standalone operation,
CComObject makes certain assumptions about where the
object's memory has been allocated from (the heap) and whether the
existence of the object should keep the server loaded (it does).
For other needs, ATL provides five more classes meant to be the
most derived class in your implementation hierarchy:
CComObjectCached, CComObjectNoLock,
CComObjectGlobal, CComObjectStack, and
CComObjectStackEx.
CComObjectCached
CComObjectCached objects implement
reference counting, assuming that you're going to create an
instance and then hold it for the life of the server, handing out
references to it as requested. To avoid keeping the server running
forever after the cached instance is created, the boundary for
keeping the server running is a reference count of one, although
the lifetime of the object is still managed on a boundary of
zero:
template <class Base>
class CComObjectCached : public Base {
public:
...
STDMETHOD_(ULONG, AddRef)() {
ULONG l = InternalAddRef();
if (l == 2)
_pAtlModule->Lock();
return l;
}
STDMETHOD_(ULONG, Release)() {
ULONG l = InternalRelease();
if (l == 0)
delete this;
else if (l == 1)
_pAtlModule->Unlock();
return l;
}
...
};
Cached objects are useful for in-process class
objects:
static CComObjectCached<CPenguinCO>* g_pPenguinCO = 0;
BOOL WINAPI DllMain(HINSTANCE, DWORD dwReason, void*) {
switch( dwReason ) {
case DLL_PROCESS_ATTACH:
g_pPenguinCO = new CComObjectCached<CPenguinCO>();
// 1st ref. doesn't keep server alive
if( g_pPenguinCO ) g_pPenguinCO->AddRef();
break;
case DLL_PROCESS_DETACH:
if( g_pPenguinCO ) g_pPenguinCO->Release();
break;
}
return TRUE;
}
STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid,
void** ppv) {
// Subsequent references do keep server alive
if( clsid == CLSID_Penguin && g_pPenguinCO )
return g_pPenguinCO->QueryInterface(riid, ppv);
return CLASS_E_CLASSNOTAVAILABLE;
}
CComObjectNoLock
Sometimes you don't want outstanding references
on your object to keep the server alive. For example, class objects
in an out-of-process server are cached in a table maintained by
ole32.dll some number of
times (it might not be one). For this reason, COM itself
manages how the lifetime of a class object affects the lifetime of its
out-of-process server using the LockServer method of the
IClassFactory interface. For this use, ATL provides
CComObjectNoLock, whose implementation does not affect the
lifetime of the server:
template <class Base>
class CComObjectNoLock : public Base {
public:
...
STDMETHOD_(ULONG, AddRef)()
{ return InternalAddRef(); }
STDMETHOD_(ULONG, Release)() {
ULONG l = InternalRelease();
if (l == 0) delete this;
return l;
}
...
};
No-lock objects are useful for out-of-process
class objects:
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
CoInitialize(0);
CComObjectNoLock<CPenguinCO>* pPenguinCO =
new CComObjectNoLock<CPenguinCO>();
if( !pPenguinCO ) return E_OUTOFMEMORY;
pPenguinCO->AddRef();
DWORD dwReg;
HRESULT hr;
// Reference(s) cached by ole32.dll won't keep server
// from shutting down
hr = CoRegisterClassObject(CLSID_Penguin, pPenguinCO, ...,
&dwReg);
if( SUCCEEDED(hr) ) {
MSG msg; while( GetMessage(&msg, 0, 0, 0) ) DispatchMessage(&msg);
CoRevokeClassObject(dwReg);
pPenguinCO->Release();
}
CoUninitialize();
return hr;
}
CComObjectGlobal
Just as it's handy to have
an object whose existence or outstanding references don't keep the
server alive, sometimes it's handy to have an object whose lifetime
matches that of the server. For example, a global or static object
is constructed once when the server is loaded and is not destroyed
until after WinMain or DllMain has completed.
Clearly, the mere existence of a global object cannot keep the
server running, or the server could never be shut down. On the
other hand, we'd like to be able to keep the server running if
there are outstanding references to a global object. For this, we
have CComObjectGlobal:
template <class Base>
class CComObjectGlobal : public Base {
public:
...
STDMETHOD_(ULONG, AddRef )() { return _pAtlModule->Lock(); }
STDMETHOD_(ULONG, Release)() { return _pAtlModule->Unlock(); }
...
};
Global objects can be used instead of cached
objects for implementing in-process class objects, but they're
useful for any global or static object:
// No references yet, so server not forced to stay alive
static CComObjectGlobal<CPenguinCO> g_penguinCO;
STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid,
void** ppv) {
// All references keep the server alive
if( clsid == CLSID_Penguin )
return g_penguinCO.QueryInterface(riid, ppv);
return CLASS_E_CLASSNOTAVAILABLE;
}
CComObjectStack
and CComObjectStackEx
Instead of using a global or static object, you
might find yourself with the urge to allocate a COM object on the
stack. ATL supports this technique with
CComObjectStack:
template <class Base>
class CComObjectStack : public Base {
public:
...
STDMETHOD_(ULONG, AddRef)()
{ ATLASSERT(FALSE); return 0; }
STDMETHOD_(ULONG, Release)()
{ ATLASSERT(FALSE); return 0; }
STDMETHOD(QueryInterface)(REFIID iid, void** ppvObject)
{ ATLASSERT(FALSE); return E_NOINTERFACE; }
...
};
Based on the implementation, it should be clear
that you're no longer doing COM. CComObjectStack shuts up
the compiler, but you still cannot use any methods of
IUnknown, which means that you cannot pass out an
interface reference from an object on the stack. This is good
because, as with a reference to anything on the stack, as soon as
the stack goes away, the reference points at garbage. The nice
thing about ATL's implementation of CComObjectStack is
that it warns you at runtime that you're doing something bad:
void DoABadThing(IBird** ppbird) {
CComObjectStack<CPenguin> penguin;
penguin.Fly(); // Using IBird method is OK
penguin.StraightenTie(); // Using ISnappyDresser method
// also OK
// This will trigger an assert at runtime
penguin.QueryInterface(IID_IBird, (void**)ppbird);
}
CComObjectStackEx addresses the
limitations of CComObjectStack by providing a more useful
implementation of IUnknown:
template <class Base>
class CComObjectStackEx : public Base {
public:
typedef Base _BaseClass;
CComObjectStackEx(void* = NULL) {
#ifdef _DEBUG
m_dwRef = 0;
#endif
m_hResFinalConstruct = _AtlInitialConstruct();
if (SUCCEEDED(m_hResFinalConstruct))
m_hResFinalConstruct = FinalConstruct();
}
virtual ~CComObjectStackEx() {
// This assert indicates mismatched ref counts.
//
// The ref count has no control over the
// lifetime of this object, so you must ensure
// by some other means that the object remains
// alive while clients have references to its interfaces.
ATLASSUME(m_dwRef == 0);
FinalRelease();
#ifdef _ATL_DEBUG_INTERFACES
_AtlDebugInterfacesModule.DeleteNonAddRefThunk(
_GetRawUnknown());
#endif
}
STDMETHOD_(ULONG, AddRef)() {
#ifdef _DEBUG
return InternalAddRef();
#else
return 0;
#endif
}
STDMETHOD_(ULONG, Release)() {
#ifdef _DEBUG
return InternalRelease();
#else
return 0;
#endif
}
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {
return _InternalQueryInterface(iid, ppvObject);
}
HRESULT m_hResFinalConstruct;
};
As you can see, CComObjectStackEx
permits the use of the IUnknown methods, as long as they
are called within the scope of the CComObjectStackEx
instance. This allows methods called from within the instance scope
to treat the object as if it were a typical heap-based COM object,
as in the following:
void PlayWithBird() {
CComObjectStackEx<CPenguin> penguin;
IBird* pBird = NULL;
penguin.QueryInterface(IID_IBird,
(void**)&pBird); // OK -> no assert
DoBirdTrickes(pBird);
}
void DoBirdTricks(IBird* pBird) {
pBird->Fly(); // IBird methods OK
ISnappyDresser* pPenguin = NULL;
pBird->QueryInterface(IID_ISnappyDresser,
(void**)&pPenguin); // OK
pPenguin->StraightenTie(); // ISnappyDresser methods OK
pPenguin->Release(); // OK -> no assert
}
One from Column A,
Two from Column B. . .
Table 4.2 shows the various identity and lifetime
options ATL provides.
Table 4.2. ATL's Identity and Lifetime
Options
Class
|
Standalone or Aggregated
|
Heap or Stack
|
Existence Keeps Server Alive
|
Extent Refs Keep Server Alive
|
Useful IUnkown Methods
|
CcomObject
|
Standalone
|
Heap
|
Yes
|
Yes
|
Yes
|
CComAggObject
|
Aggregated
|
Heap
|
Yes
|
Yes
|
Yes
|
CComPolyObject
|
Standalone or aggregated
|
Heap
|
Yes
|
Yes
|
Yes
|
CComObjectCached
|
Standalone
|
Heap
|
No
|
Second Reference
|
Yes
|
CComObjectNoLock
|
Standalone
|
Heap
|
No
|
No
|
Yes
|
CComObjectGlobal
|
Standalone
|
Data seg.
|
No
|
Yes
|
Yes
|
CComObjectStack
|
Standalone
|
Stack
|
No
|
No
|
No
|
CComObjectStackEx
|
Standalone
|
Stack
|
No
|
No
|
Yes
|
|