previous page
next page

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[4]:

[4] As an optimization, ATL provides a specialization of CComObjectRootEx<CComSingleThreadModel> that does not have a _CritSec object, side-stepping minimum object size requirements imposed by the compiler.

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.[5] 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.

[5] Chapter 5, "COM Servers," describes all these uses of the interface map.

Figure 4.2. CPenguin object layout, including vptrs to vtbls


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.

Figure 4.3. CPenguin interface map, CPenguin object, and vtbls


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.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)