The Core of
IUnknown
Standalone
Reference Counting
To encapsulate the Lock and
Unlock methods as well as the "just thread-safe enough"
reference counting, ATL provides the CComObjectRootEx base
class, parameterized by the desired threading model:
template <class ThreadModel>
class CComObjectRootEx : public CComObjectRootBase {
public:
typedef ThreadModel _ThreadModel;
typedef typename _ThreadModel::AutoCriticalSection _CritSec;
typedef typename _ThreadModel::AutoDeleteCriticalSection _AutoDelCritSec;
typedef CComObjectLockT<_ThreadModel> ObjectLock;
~CComObjectRootEx() {}
ULONG InternalAddRef() {
ATLASSERT(m_dwRef != -1L);
return _ThreadModel::Increment(&m_dwRef);
}
ULONG InternalRelease() {
#ifdef _DEBUG
LONG nRef = _ThreadModel::Decrement(&m_dwRef);
if (nRef < -(LONG_MAX / 2)) {
ATLASSERT(0 &&
_T("Release called on a pointer that has"
" already been released"));
}
return nRef;
#else
return _ThreadModel::Decrement(&m_dwRef);
#endif
}
HRESULT _AtlInitialConstruct() { return m_critsec.Init(); }
void Lock() {m_critsec.Lock();}
void Unlock() {m_critsec.Unlock();}
private:
_AutoDelCritSec m_critsec;
};
template <>
class CComObjectRootEx<CComSingleThreadModel>
: public CComObjectRootBase {
public:
typedef CComSingleThreadModel _ThreadModel;
typedef _ThreadModel::AutoCriticalSection _CritSec;
typedef _ThreadModel::AutoDeleteCriticalSection
_AutoDelCritSec;
typedef CComObjectLockT<_ThreadModel> ObjectLock;
~CComObjectRootEx() {}
ULONG InternalAddRef() {
ATLASSERT(m_dwRef != -1L);
return _ThreadModel::Increment(&m_dwRef);
}
ULONG InternalRelease() {
#ifdef _DEBUG
long nRef = _ThreadModel::Decrement(&m_dwRef);
if (nRef < -(LONG_MAX / 2)) {
ATLASSERT(0 && _T("Release called on a pointer "
"that has already been released"));
}
return nRef;
#else
return _ThreadModel::Decrement(&m_dwRef);
#endif
}
HRESULT _AtlInitialConstruct() { return S_OK; }
void Lock() {}
void Unlock() {}
};
ATL classes derive from
CComObjectRootEx and forward AddRef and
Release calls to the InternalAddRef and
InternalRelease methods when the object is created
standalone (that is, not aggregated). Note that
InternalRelease checks the decremented reference count
against the somewhat odd-looking value (LONG_MAX / 2). The
destructor of CComObject (or one of its alternatives,
discussed a bit later) sets the reference count to this value. The
ATL designers could have used a different value here, but basing
the value on LONG_MAX makes it unlikely that such a
reference count could be reached under normal circumstances.
Dividing LONG_MAX by 2 ensures that the resulting value
can't mistakenly be reached by wrapping around from 0.
InternalRelease simply checks the reference count against
this value to see if you're trying to call Release on an
object that has already been destroyed. If so, an assert is issued
in debug builds.
The template specialization for
CComSingleThreadModel demonstrates the "just safe enough"
multithreading. When used in a single-threaded object, the
Lock and Unlock methods do nothing, and no
critical section object is created.
With the Lock and Unlock
methods so readily available in the base class, you might be
tempted to write the following incorrect code:
class CPenguin
: public CComObjectRootEx<CComMultiThreadModel>, ... {
STDMETHODIMP get_Wingspan(long* pnWingspan) {
Lock();
if( !pnWingspan ) return E_POINTER; // Forgot to Unlock
*pnWingSpan = m_nWingspan;
Unlock();
return S_OK;
}
...
};
To help you avoid this kind of mistake,
CComObjectRootEx provides a type definition for a class
called ObjectLock, based on CComObjectLockT
parameterized by the threading model:
template <class ThreadModel>
class CcomObjectLockT {
public:
CComObjectLockT(CComObjectRootEx<ThreadModel>* p) {
if (p)
p->Lock();
m_p = p;
}
~CComObjectLockT() {
if (m_p)
m_p->Unlock();
}
CComObjectRootEx<ThreadModel>* m_p;
};
template <>
class CComObjectLockT<CComSingleThreadModel> {
public:
CComObjectLockT(CComObjectRootEx<CComSingleThreadModel>*) {}
~CComObjectLockT() {}
};
Instances of
CComObjectLockT Lock the object passed to the constructor
and Unlock it upon destruction. The ObjectLock
type definition provides a convenient way to write code that will
properly release the lock regardless of the return path:
class CPenguin
: public CComObjectRootEx<CComMultiThreadModel>, ... {
STDMETHODIMP get_Wingspan(long* pnWingspan) {
ObjectLock lock(this);
if( !pnWingspan ) return E_POINTER; // Unlock happens as
// stack unwinds
*pnWingSpan = m_nWingspan;
return S_OK;
}
...
};
Of course, the specialization for
CComSingleThreadModel ensures that in the single-threaded
object, no locking is done. This is useful when you've changed your
threading model; you don't pay a performance penalty for using an
ObjectLock if you don't actually need one.
Table-Driven
QueryInterface
In addition to "just
thread-safe enough" implementations of AddRef and
Release for standalone COM objects,
CComObjectRootEx (via its base class,
CComObject-RootBase) provides a static, table-driven
implementation of QueryInterface called
InternalQueryInterface:
static HRESULT WINAPI
CComObjectRootBase::InternalQueryInterface(
void* pThis,
const _ATL_INTMAP_ENTRY* pEntries,
REFIID iid,
void** ppvObject);
This function's job is to use the this
pointer of the object, provided as the pThis parameter,
and the requested interface to fill the ppvObject
parameter with a pointer to the appropriate virtual function table
pointer (vptr). It does this using the pEntries
parameter, a zero-terminated array of _ATL_INTMAP_ENTRY
structures:
struct _ATL_INTMAP_ENTRY {
const IID* piid;
DWORD dw;
_ATL_CREATORARGFUNC* pFunc;
};
Each interface exposed from a COM object is one
entry in the interface map, which is a class static array of
_ATL_INTMAP_ENTRY structures. Each entry consists of an
interface identifier, a function pointer, and an argument for the
function represented as a DWORD. This provides a flexible,
extensible mechanism for implementing QueryInterface that
supports multiple inheritance, aggregation, tear-offs, nested
composition, debugging, chaining, and just about any other wacky
COM identity tricks C++ programmers currently use.
However, because most interfaces are implemented using multiple
inheritance, you don't often need this much flexibility. For
example, consider one possible object layout for instances of the
CPenguin class, shown in Figure 4.2.
class CPenguin : public IBird, public ISnappyDresser {...};
The typical
implementation of QueryInterface for a class using
multiple inheritance consists of a series of if statements
and static_cast operations; the purpose is to adjust the
this pointer by some fixed offset to point to the
appropriate vptr. Because the offsets are known at compile
time, a table matching interface identifiers to offsets would
provide an appropriate data structure for adjusting the
this pointer at runtime. To support this common case,
InternalQueryInterface function treats the
_ATL_INTMAP_ENTRY as a simple IID/offset pair if the
pFunc member has the special value
_ATL_SIMPLEMAPENTRY:
#define _ATL_SIMPLEMAPENTRY ((ATL::_ATL_CREATORARGFUNC*)1)
To be able to use the
InternalQueryInterface function, each implementation
populates a static interface map. To facilitate populating this
data structure, and to provide some other methods used internally,
ATL provides the following macros (as well as others described in
Chapter 6, "Interface
Maps"):
#define BEGIN_COM_MAP(class) ...
#define COM_INTERFACE_ENTRY(itf) ...
#define END_COM_MAP() ...
For example, our CPenguin class would
declare its interface map like this:
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()
...
};
In an abbreviated form, this would expand to the
following:
class CPenguin :
public CComObjectRootEx<CComMultiThreadModel>,
public IBird,
public ISnappyDresser {
...
public:
IUnknown* GetUnknown() {
ATLASSERT(_GetEntries()[0].pFunc == _ATL_SIMPLEMAPENTRY);
return (IUnknown*)((int)this+_GetEntries()->dw); }
}
HRESULT _InternalQueryInterface(REFIID iid, void** ppvObject) {
return InternalQueryInterface(this, _GetEntries(), iid, ppvObject);
}
const static _ATL_INTMAP_ENTRY* WINAPI _GetEntries() {
static const _ATL_INTMAP_ENTRY _entries[] = {
{ &_ATL_IIDOF(IBird), 0, _ATL_SIMPLEMAPENTRY },
{ &_ATL_IIDOF(ISnappyDresser), 4, _ATL_SIMPLEMAPENTRY },
{ 0, 0, 0 }
};
return _entries;
}
...
};
The _ATL_IIDOF macro expands as
follows:
#ifndef _ATL_NO_UUIDOF
#define _ATL_IIDOF(x) __uuidof(x)
#else
#define _ATL_IIDOF(x) IID_##x
#endif
This macro lets you choose to use
__uuidof operator or the standard naming convention to
specify the IID for the interface in question for the entire
project.
Figure
4.3 shows how this interface map relates to an instance of a
CPenguin object in memory.
Something else worth mentioning is the
GetUnknown member function that the BEGIN_COM_MAP
provides. Although ATL uses this internally, it's also useful when
passing your this pointer to a function that requires an
IUnknown*. Because your class derives from potentially
more than one interface, each of which derives from
IUnknown, the compiler considers passing your own
this pointer as an IUnknown* to be ambiguous.
HRESULT FlyInAnAirplane(IUnknown* punkPassenger);
// Penguin.cpp
STDMETHODIMP CPenguin::Fly() {
return FlyInAnAirplane(this); // ambiguous
}
For these situations, GetUnknown is your friend,
e.g.
STDMETHODIMP CPenguin::Fly() {
return FlyInAnAirplane(this->GetUnknown()); // unambiguous
}
As you'll see later, GetUnknown is
implemented by handing out the first entry in the interface
map.
Support for
Aggregation: The Controlled Inner
So far, we've discussed the implementation of
IUnknown for standalone COM objects. However, if our
object is to participate in aggregation as a controlled inner,
our job is not to think for ourselves, but
rather to be subsumed by the thoughts and prayers of another. A
controlled inner does this by blindly forwarding all calls on the
publicly available implementation of IUnknown to the
controlling outer's implementation. The controlling outer's
implementation is provided as the pUnkOuter argument to
the CreateInstance method of IClassFactory. If
our ATL-based COM object is used as a controlled inner, it simply
forwards all calls to IUnknown methods to the
OuterQueryInterface, OuterAddRef, and
OuterRelease functions provided in
CComObjectRootBase; these, in turn, forward to the
controlling outer. The relevant functions of
CComObjectRootBase are shown here:
class CComObjectRootBase {
public:
CComObjectRootBase() { m_dwRef = 0L; }
...
ULONG OuterAddRef() {
return m_pOuterUnknown->AddRef();
}
ULONG OuterRelease() {
return m_pOuterUnknown->Release();
}
HRESULT OuterQueryInterface(REFIID iid, void ** ppvObject) {
return m_pOuterUnknown->QueryInterface(iid, ppvObject);
}
...
union {
long m_dwRef;
IUnknown* m_pOuterUnknown;
};
};
Notice that CComObjectRootBase keeps
the object's reference count and a pointer to a controlling unknown
as a union. This implies that an object can either maintain its own
reference count or be aggregated,
but not both at the same time. This implication is not true. If the
object is being aggregated, it must maintain a reference count
and a pointer to a controlling
unknown. In this case, discussed more later, ATL keeps the
m_pUnkOuter in one instance of the CComObjectBase
and derives from CComObjectBase again to keep the object's
reference count.
More to Come
Although it's possible to implement the methods
of IUnknown directly in your class using the methods of
the base class CComObjectRootEx, most ATL classes don't.
Instead, the actual implementations of the IUnknown
methods are left to a class that derives from your class, as in CComObject.
We discuss this after we talk about the responsibilities of your
class.
|