"COM-Lite" 101

 

Motivation

Object lifetime management (when to free an object) and object self-description (how to determine object capabilities at runtime) are two fundamental problems that a C++ object-oriented architecture using abstract interfaces must deal with.  For example, consider the following problematic piece of code.

    class CPerson { ... };
    class CSinger : CPerson { ... };
    class CDancer : CPerson { ... };
    AddToPerformersList(CPerson *pperson);
    AddToPayrollList(CPerson *pperson);
    
    // so far so good ...

    CSinger *psinger = new CSinger();
    AddToPerformersList(&singer);
    AddToPayrollList(&singer);
    
    // PROBLEM: who is responsible for freeing psinger ?
    
    CDancer *pdancer = new CDancer();
    AddToPerformersList(&dancer);
    AddToPayrollList(&dancer);
    
    // PROBLEM: who is responsible for freeing pdancer ?

    HavePerformersDoTheirThing();
    
    // PROBLEM: how does this function know what each performer can do?

We are lacking here a mechanism for managing the lifetime of the objects and making them self-descriptive about their capabilities.  The C++ language itself does not provide such mechanisms.  But we would still like to avoid making something up ad-hoc. 

Microsoft COM provides a nice solution with the concept of the IUnknown interface.  However, the full COM requires that the program have a setup utility to register objects (something we would like to avoid in a set of command-line tools), requires an emulation layer on UNIX, and generally is much more heavyweight than we need .  Hence, we have chosen a small, portable subset of COM, which we call "COM-Lite", to employ in the xmltk architecture.  This subset is just the interface IUnknown, along with its associated data structures, helper functions, and conventions.

IUnknown

IUnknown is the fundamental interface in COM-Lite, as in COM.  All other COM-Lite interfaces must derive from it.  Here is the definition.

class IUnknown
{
    virtual HRESULT QueryInterface(REFIID riid, void** ppvObj) = 0;
    virtual ULONG AddRef() = 0;
    virtual ULONG Release() = 0;
};
AddRef and Release are used in the obvious way for reference counting.  QueryInterface is used for object self-description.  Every interface in COM has a globally-unique identifier (known as an IID), which is just a 128-bit number.  That is the REFIID parameter.   QueryInterface allows you to ask an object for an interface pointer by its IID.  If the interface is implemented by the object, the pointer is returned in ppvObj.

Conventions

There are some rules regarding the use of IUnknown.

  1. QueryInterface on particular object for IID_IUnknown must always return the same object pointer.  This is for the purpose of object identity.  See next rule for more explanation.
  2. Object comparison must be done using the canonical IUnknown.  The canonical IUnknown is the one returned by QueryInterface for IID_IUnknown, versus one obtained by casting some other interface to IUnknown.
        bool IsSamePerson(IPerson* pp, IGraduateStudent* pgs)
        {
            return (safecast<IUnknown*>(ps) == safecast<IUnknown*>(pgs));    // *incorrect*
        }
    
        bool IsSamePerson(IPerson* pp, IGraduateStudent* pgs)
        {
            IUnknown* punkPerson, IUnknown* punkGradStudent;
    
            ps1->QueryInterface(IID_IUnknown, (void**)&punkPerson);
            ps2->QueryInterface(IID_IUnknown, (void**)&punkGradStudent);
    
            bool bResult = (punkPerson == punkGradStudent);
    
            punkPerson->Release();
            punkGradStudent->Release();
    
            return bResult;                                                  // *correct*
        }
        
    An object that implements n interfaces (in other words, multiply-inherits from n interfaces) actually has n implementations of IUnknown, one cannonical implementation (the one you wrote in code), and n-1 stubs generated by the C++ compiler that just jump to the cannonical implementation.  If you cast an interface to IUnknown, you do not know if you are casting to some random stub, or to the cannonical.  To be certain, you need to use QueryInterface.

    Furthermore, there is no general requirement in COM that two calls to QueryInterface for the same interface return the same interface pointer.  An object may implement the interface as a tear-off class which is allocated and returned for every QueryInterface call, in which case you might have many different pointers for the same object.  Hence, even if you have two pointers to the same interface, you are not supposed to compare them.  IUnknown is the only interface that is not allowed to be a tear-off, according to rule #1 above.

     

  3.  

Miscellaneous Details

You may have noticed that the names of COM interfaces seem to begin with "I".  This is according to the conventions of the Hungarian Notation naming scheme, extended for COM.

There are other flavors of these unique-identifiers in COM too (CLSID, SID, CATID, etc).  Each of these has its own semantics, but all are syntactically equivalent to the generic type GUID.  Here is the GUID structure definition.

typedef struct _GUID 
{ 
    unsigned long        Data1; 
    unsigned short       Data2; 
    unsigned short       Data3; 
    unsigned char        Data4[8]; 
} GUID; 

typedef GUID IID;
typedef const IID* REFIID;

 

Singers and Dancers Revisited 

Here is how the example would look using COM-Lite.

 

Other Useful Interfaces

Another problem that arises in object-oriented architectures is that of circular references.  Here is the problem.  One object has a reference to another object and wants to give that object a back-pointer for some purpose.  Later, it wants to tell that object to break the cycle and release the back-pointer.  How can it accomplish these tasks?  The standard way is to use the interface IObjectWithSite.

class IObjectWithSite : IUnknown
{
    HRESULT SetSite(IUnknown* pUnkSite);
    HRESULT GetSite(REFIID riid, void ** ppvSite);
};

XMLTK interfaces