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:
- Verify that the COM Library is a compatible version with the COM function CoBuildVersion.
- Initialize the COM Library before using any other functions in it by calling CoInitialize.
- 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.