Export (0) Print
Expand All

The Rules of the Component Object Model

 

Charlie Kindel
Program Manager, Windows NT

October 20, 1995

Abstract

This paper is intended to be a quick reference for the primary rules of using and implementing Microsoft® Component Object Model (COM) objects. Readers interested in gaining a better understanding of what COM is, as well as the motivations behind its design and philosophy, should read the first two chapters of the Component Object Model Specification (MSDN Library, Specifications). Chapter 1 is a brief introduction, and Chapter 2 provides a thorough overview. The information presented here is all taken from the COM specification.

Rule #1: Must Implement IUnknown

An object is not a Microsoft® Component Object Model (COM) object unless it implements at least one interface that at minimum is IUnknown.

Interface Design Rules

  • Interfaces must directly or indirectly inherit from IUnknown.
  • Interfaces must have a unique interface identifier (IID).
  • Interfaces are immutable. Once assigned an IID and published, no element of the interface definition may change.
  • Interface member functions should have a return type of HRESULT to allow the remoting infrastructure to report remote procedure call (RPC) errors.
  • String parameters in interface member functions should be Unicode™.

Implementing IUnknown

  • Object identity. It is required that any call to QueryInterface on any interface for a given object instance for the specific interface IUnknown must always return the same physical pointer value. This enables calling QueryInterface(IID_IUnknown, ...) on any two interfaces and comparing the results to determine whether they point to the same instance of an object (the same COM object identity).
  • Static interface set. It is required that the set of interfaces accessible on an object via QueryInterface be static, not dynamic. That is, if QueryInterface succeeds for a given IID once, it will always succeed on subsequent calls on the same object (except in catastrophic failure situations), and if QueryInterface fails for a given IID, subsequent calls for the same IID on the same object must also fail.
  • Object integrity. QueryInterface must be reflexive, symmetric, and transitive with respect to the set of interfaces that are accessible. That is, given the code snippet below:
    IA * pA = (some function returning an IA *);
    IB * pB = NULL;
    HRESULT hr;
    hr = pA->QueryInterface(IID_IB, &pB); // line 4
    
    
Symmetric:pA->QueryInterface(IID_IA, ...) must succeed (a>>a)
Reflexive:If, in line 4, pB was successfully obtained, then
  pB->QueryInterface(IID_IA, ...)

must succeed (a>>b, then b>>a).

Transitive:If, in line 4, pB was successfully obtained, and we do
  IC * pC = NULL;
  hr = pB->QueryInterface(IID_IC, &pC);    //Line 7

and pC is successfully obtained in line 7, then

  pA->QueryInterface(IID_IC, ...)

must succeed (a>>b, and b>>c, then a>>c).

  • Minimum reference counter size. AddRef implementations are required to maintain a counter that is large enough to support 2 31 –1 outstanding pointer references to all the interfaces on a given object taken as a whole. A 32-bit unsigned integer fits this requirement.
  • Release cannot indicate failure. If a client needs to know that resources have been freed, and so forth, it must use a method in some interface on the object with higher-level semantics before calling Release.

Memory Management Rules

  • The lifetime management of pointers to interfaces is always accomplished through the AddRef and Release methods found on every COM interface. (See "Reference-Counting Rules" below.)
  • The following rules apply to parameters to interface member functions, including the return value, that are not passed "by-value":
    • For in parameters, the caller should allocate and free the memory.
    • The out parameters must be allocated by the callee and freed by the caller using the standard COM memory allocator.
    • The in-out parameters are initially allocated by the caller, then freed and re-allocated by the callee if necessary. As with out parameters, the caller is responsible for freeing the final returned value. The standard COM memory allocator must be used.
  • If a function returns a failure code, then in general the caller has no way to clean up the out or in-out parameters. This leads to a few additional rules:
    • In error returns, out parameters must always be reliably set to a value that will be cleaned up without any action on the caller's part.
    • Further, it is the case that all out pointer parameters (including pointer members of a caller-allocate callee-fill structure) must explicitly be set to NULL. The most straightforward way to ensure this is (in part) to set these values to NULL on function entry.
    • In error returns, all in-out parameters must either be left alone by the callee (and thus remaining at the value to which it was initialized by the caller; if the caller didn't initialize it, then it's an out parameter, not an in-out parameter) or be explicitly set as in the out parameter error return case.

Reference-Counting Rules

Rule 1: AddRef must be called for every new copy of an interface pointer, and Release called for every destruction of an interface pointer, except where subsequent rules explicitly permit otherwise.

The following rules call out common nonexceptions to Rule 1.

  • Rule 1a: In-out-parameters to functions. The caller must AddRef the actual parameter, since it will be Released by the callee when the out-value is stored on top of it.
  • Rule 1b: Fetching a global variable. The local copy of the interface pointer fetched from an existing copy of the pointer in a global variable must be independently reference counted, because called functions might destroy the copy in the global while the local copy is still alive.
  • Rule 1c: New pointers synthesized out of "thin air." A function that synthesizes an interface pointer using special internal knowledge, rather than obtaining it from some other source, must do an initial AddRef on the newly synthesized pointer. Important examples of such routines include instance creation routines, implementations of IUnknown::QueryInterface, and so on.
  • Rule 1d: Returning a copy of an internally stored pointer. After the pointer has been returned, the callee has no idea how its lifetime relates to that of the internally stored copy of the pointer. Thus, the callee must call AddRef on the pointer copy before returning it.

Rule 2: Special knowledge on the part of a piece of code of the relationships of the beginnings and the endings of the lifetimes of two or more copies of an interface pointer can allow AddRef/Release pairs to be omitted.

  • From a COM client's perspective, reference-counting is always a per-interface concept. Clients should never assume that an object uses the same reference count for all interfaces.
  • The return values of AddRef and Release should not be relied upon, and should be used only for debugging purposes.
  • Pointer stability; see details in the OLE Help file under "Reference-Counting Rules," subsection "Stabilizing the this Pointer and Keeping it Valid."

See the excellent "Managing Object Lifetimes in OLE" technical article by Douglas Hodges, and Chapter 3 of Inside OLE, 2nd edition, by Kraig Brockschmidt (MSDN Library, Books) for more information on reference-counting.

COM Application Responsibilities

Each process that uses COM in any way—client, server, object implementor—is responsible for three things:

  1. Verify that the COM Library is a compatible version with the COM function CoBuildVersion.
  2. Initialize the COM Library before using any other functions in it by calling CoInitialize.
  3. Uninitialize the COM Library when it is no longer in use by CoUninitialize.

In-process servers can assume that the process they are being loaded into has already performed these steps.

Server Rules

  • In-process servers must export DllGetClassObject and DllCanUnloadNow.
  • In-process servers must support COM self-registration.
    • In-process and local servers should put an OLESelfReg string in their file version information.
    • In-process servers should export DllRegisterServer and DllUnRegisterServer.
    • Local servers should support the /RegServer and /UnRegServer command-line switches.

Creating Aggregatable Objects

Creating objects that can be aggregated is optional; however, it is simple to do, and doing so has significant benefits. The following rules must be followed in order to create an object that is aggregatable (often called the inner object).

  • The inner object's implementation of QueryInterface, AddRef, and Release for the IUnknown interface controls the inner object's reference count alone, and must not delegate to the outer unknown. This IUnknown implementation is called the implicit IUnknown.
  • The implementation of QueryInterface, AddRef, and Release members of all interfaces that the inner object implements, other than IUnknown itself, must delegate to the outer unknown. These implementations must not directly affect the inner object's reference count.
  • The implicit IUnknown must implement the QueryInterface behavior for only the inner object.
  • The aggregatable object must not call AddRef when holding a reference to the outer unknown pointer.
  • If, when the object is created, any interface other than IUnknown is requested, the creation must fail with E_UNKNOWN.

The code fragment below illustrates a correct implementation of an aggregatable object using the nested class approach to implementing interfaces:

// CSomeObject is an aggregatable object that implements
// IUnknown and ISomeInterface
class CSomeObject : public IUnknown
{
    private:
        DWORD         m_cRef;            // Object reference count
        IUnknown*    m_pUnkOuter;        // Outer unknown, no AddRef

        // Nested class to implement the ISomeInterface interface
        class CImpSomeInterface : public ISomeInterface
        {
            friend class CSomeObject ;
            private:
                DWORD           m_cRef;    // Interface ref-count, for debugging
                IUnknown*    m_pUnkOuter;  // Outer unknown, for delegation
            public:
                CImpSomeInterface() { m_cRef = 0;    };
                ~ CImpSomeInterface(void) {};

                // IUnknown members delegate to the outer unknown

                // IUnknown members do not control lifetime of object

                STDMETHODIMP     QueryInterface(REFIID riid, void** ppv)
                {    return m_pUnkOuter->QueryInterface(riid,ppv);  };

                STDMETHODIMP_(DWORD) AddRef(void)
                {     return m_pUnkOuter->AddRef();   };

                STDMETHODIMP_(DWORD) Release(void)
                {     return m_pUnkOuter->Release();   };

                // ISomeInterface members
                STDMETHODIMP SomeMethod(void) 
                {    return S_OK;    };
        } ;
        CImpSomeInterface m_ImpSomeInterface ;
    public:
        CSomeObject(IUnknown * pUnkOuter)
        {
            m_cRef=0;

            // No AddRef necessary if non-NULL as we're aggregated.

            m_pUnkOuter=pUnkOuter;
            m_ImpSomeInterface.m_pUnkOuter=pUnkOuter;    
        } ;
        ~CSomeObject(void) {} ;

        // Static member function for creating new instances (don't use

        // new directly). Protects against outer objects asking for interfaces 

        // other than IUnknown

        static HRESULT Create(IUnknown* pUnkOuter, REFIID riid, void **ppv)
        {
            CSomeObject*        pObj;
            if (pUnkOuter != NULL && riid != IID_IUnknown)
                return CLASS_E_NOAGGREGATION;
            pObj = new CSomeObject(pUnkOuter);
            if (pObj == NULL)
                return E_OUTOFMEMORY;
            // Set up the right unknown for delegation (the non-aggregation 
                                                        case)
            if (pUnkOuter == NULL)
                pObj->m_pUnkOuter = (IUnknown*)pObj ;
            HRESULT hr;
            if (FAILED(hr = pObj->QueryInterface(riid, (void**)ppv)))
                delete pObj ;
            return hr;
        }

        // Implicit IUnknown members, non-delegating

        // Implicit QueryInterface only controls inner object

        STDMETHODIMP QueryInterface(REFIID riid, void** ppv)
        {
            *ppv=NULL;
            if (riid == IID_IUnknown)   
                *ppv=this;
            if (riid == IID_ISomeInterface)  
                *ppv=&m_ImpSomeInterface;
            if (NULL==*ppv)  
                return ResultFromScode(E_NOINTERFACE);
            ((IUnknown*)*ppv)->AddRef();
            return NOERROR;
        } ;
        STDMETHODIMP_(DWORD) AddRef(void)
        {      return ++m_cRef; };
        STDMETHODIMP_(DWORD) Release(void)
        {    
            if (--m_cRef != 0)
                return m_cRef;
            delete this;
            return 0;
        };
};

Aggregating Objects

When developing an object that aggregates in another object, these rules must be followed:

  • When creating the inner object, the outer object must explicitly ask for IUnknown.
  • The outer object must protect its implementation of Release from reentrancy with an artificial reference count around its destruction code.
  • The outer object must call its own outer unknown's Release if it queries for a pointer to any of the inner object's interfaces. To free this pointer, the outer object calls its own outer unknown's AddRef followed by Release on the inner object's pointer:
    // Obtaining inner object interface pointer
    pUnkInner->QueryInterface(IID_IFoo, &pIFoo);
    pUnkOuter->Release();
    
    // Releasing inner object interface pointer
    pUnkOuter->AddRef();
    pIFoo->Release();
    
    
  • The outer object must not blindly delegate a query for any unrecognized interface of the inner object unless that behavior is specifically the intention of the outer object.

Apartment Threading Model

The details of apartment-model threading are actually quite simple, but must be followed carefully, as follows:

Every object lives on a single thread (within a single apartment).

  • All calls to an object must be made on its thread (within its apartment). It is forbidden to call an object directly from another thread. Applications that attempt to use objects in this free-threaded manner will likely experience problems that will prevent them from running properly in future versions of the operating systems. The implication of this rule is that all pointers to objects must be marshalled between apartments.
  • Each apartment/thread with objects in it must have a message queue in order to handle calls from other processes and apartments within the same process. This means simply that the thread's work function must have a GetMessage/DispatchMessage loop. If other synchronization primitives are being used to communicate between threads, the Microsoft Win32® function MsgWaitForMultipleObjects can be used to wait for both messages and thread synchronization events.
  • DLL-based or in-process objects must be marked in the registry as "apartment aware" by adding the named value "ThreadingModel=Apartment" to their InprocServer32 key in the registration database.
  • Apartment-aware objects must write DLL entry points carefully. Each apartment that calls CoCreateInstance on an apartment-aware object will call DllGetClassObject from its thread. DllGetClassObject should therefore be able to give away multiple class objects or a single thread-safe object. Calls to CoFreeUnusedLibraries from any thread always route through the main apartment's thread to call DllCanUnloadNow.
Show:
© 2014 Microsoft