COM

Overview

Microsoft's Component Object Model (COM) is a binary standard for creating reusable software components.

It is used in many libraries that are useful in game development:

History

1987 – Dynamic Data Exchange (DDE) is introduced for inter-process communication on Windows 2.0.

1991 – Object Linking and Embedding (OLE) is introduced for compound documents. OLE was built on top of DDE and introduced with the release of Word and Excel.

1993 – COM is introduced as a replacement for DDE for the development of software components. OLE 2.0 was introduced with Windows 3.1 and built on top of COM.

1995 – DirectX is introduced as a COM API for multimedia and games.

1996 – ActiveX is introduced as a COM API for internet applications built on OLE.

Principles

Applications

COM maintains a strict separation between interface and implementation.

COM applications are built using components that are identified by class identifiers (CLSIDs).

CLSIDs are Globally Unique Identifiers (GUIDs), which are unique 128-bit number.

A COM component exposes its functionality through one or more interfaces that are identified by interface identifiers (IIDs), which are also GUIDs.

A COM class is a concrete implementation of one or more interfaces.

💡
Two COM objects can implement the same interface, and one object can implement many interfaces.

Interfaces

COM is based on the concept of interfaces that are equivalent to interfaces in C# or Java, or to pure virtual classes in C++.

To represent COM types, libraries contain COM interfaces that are defined using a language called Interface Definition Language (IDL).

IDL files define metadata for COM types in a language independent manner.

IDL files are processed by the IDL compiler, which generates C++ header files and type library (TLB) files.

TLB files contain metadata that can be processed by frameworks such as .NET.

Every COM component implement the IUnknown interface, which provides reference counting and type conversion.

Reference Counting

COM manages memory using reference counting.

Applications indicate when they are using an object and when they are done, and objects delete themselves when they are no longer needed.

COM objects maintain an internal count known as reference count.

Threading Model

In COM, the threading model is handled through the concept of apartments.

COM objects are divided into groups called apartments.

Single-threaded Apartment

COM automatic synchronize objects across multiple threads (slower).

Each single-threaded apartment must have a message loop.

COM creates a hidden window using the Windows class "OleMainThreadWndClass" in each single-threaded apartment.

Multi-threaded Apartment

COM does not perform any synchronization (faster).

The application needs to handle the synchronization.

COM Library

An application that uses COM must both initialize and uninitialize the COM library.

To initialize the COM library, call the CoInitialize or CoInitializeEx function.

The CoInitialize function initialize COM with a single-threaded apartment model.

The CoInitializeEx function provides an additional parameter to specify the apartment model.

HRESULT CoInitializeEx(
  LPVOID pvReserved,
  DWORD  dwCoInit
);

There are two common values for the dwCoInit parameter.

💡
On WinRT, you can call Windows::Foundation::Initialize instead which initializes both the Windows Runtime and the COM library.

During the applications shutdown, COM must be uninitialized by calling the CoUninitialize function.

void CoUninitialize();

Error Handling

The COM functions that need to indicate success or failure return a value of type HRESULT.

HRESULT is a 32-bit integer in which the high-order bit indicates success (1) or failure (0).

To check whether a COM method succeeds, the Windows SDK provides two macros: SUCCEEDED and FAILED.

💡
Using these macros is recommended instead of comparing the result with S_OK, or only a specific error code as there may be multiple success codes and failure codes.

Code

HRESULT hr = CoInitializeEx(nullptr, COINITBASE_MULTITHREADED);
if (FAILED(hr))
{
    // Handle the error
}
else
{
		// Success
    ...

		CoUninitialize();
}

COM Objects

After initializing the COM library, an application can create an instance a COM object that implements a COM interface.

In COM, an object or an interface is identified by a globally unique identifier (GUID) which is a 128-bit number.

This unique identifier is known as a class identifier (or CLSID) for classes, and as an interface identifier (or IID) for interfaces.

To create a COM object, you call the CoCreateInstance function with a CLSID and an IID.

HRESULT CoCreateInstance(
  REFCLSID  rclsid,
  LPUNKNOWN pUnkOuter,
  DWORD     dwClsContext,
  REFIID    riid,
  LPVOID    *ppv
);
💡
CLSIDs are symbol definitions in the header files of their corresponding library.

COM supports in-process, and out-of-process objects.

When using libraries such as DirectX and WinRT, objects are always in-process objects. The corresponding flag is CLSCTX_INPROC_SERVER.

Code

This sample code creates a COM object that implements the IWICImagingFactory interface from the Windows Imaging API.

IWICImagingFactory* wicFactory;

HRESULT hr = CoCreateInstance(
		CLSID_WICImagingFactory2,
		nullptr,
		CLSCTX_INPROC_SERVER,
    IID_IWICImagingFactory2,
		reinterpret_cast<void**>(&wicFactory));

if (SUCCEEDED(hr))
{
    // Use the object
    ...
}

Best Practices

IID

Sometimes, referencing the IID for an interface can cause linking errors in the GUID is a constant declared with external linkage.

To avoid the need to link a static library, you can use __uuidof operator which is a Microsoft language extension.

// Instead of IID_IWICImagingFactory2
__uuidof(IWICImagingFactory2)

Coercion

The type of ppv is void** and the caller must coerce the address of the pointer to a void** type.

It can be done with reinterpret_cast<void**> but it creates the potential for a type mismatch.

The IID_PPV_ARGS macro helps to avoid this error. The macro automatically inserts the IID for the interface identifier, so it is guaranteed to match the pointer type.

The macro is used in place of the last two parameters.

IWICImagingFactory* wicFactory;

HRESULT hr = CoCreateInstance(
		CLSID_WICImagingFactory2,
		nullptr,
		CLSCTX_INPROC_SERVER,
    IID_PPV_ARGS(&wicFactory));

...

IUnknown Interface

Every COM interface must inherit from an interface named IUnknown.

This interfaces supports reference counting, and obtaining instances to multiple interfaces from the same object instance.

The interface contains only three methods:

struct IUnknown
{    
    virtual HRESULT STDMETHODCALLTYPE QueryInterface( 
        REFIID riid,
        void   **ppvObject) = 0;
    
    virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
    
    virtual ULONG STDMETHODCALLTYPE Release() = 0;
};

Interfaces

In COM, one object can implement many interfaces.

However, the ability to cast an object instance to a specific interface is a language-dependent feature.

Therefore, COM provides the QueryInterface function to retrieve a specific interface from an object instance using an interface identifier.

The parameters are similar to the CoCreateInstance function.

Reference Counting

The AddRef and Release functions support the referencing counting functionality.

After creating an object instance with CoCreateInstance, the reference count of the object is set to 1.

Therefore, you must call Release when you are done using the pointer.

SafeRelease

To release a pointer safely, a helper function can be used to check if the pointer is null before calling Release, and then set the pointer to null.

template <class T>
void SafeRelease(T** ptr)
{
    if (*ptr)
    {
        (*ptr)->Release();
        *ptr = nullptr;
    }
}

ComPtr

Instead of calling Release or SafeRelease to release an object, a good practice is to wrap the reference into a smart pointer.

An advantage of using smart pointers is that the references can be stored as members of a owner class without the need to call Release in the destructor of that class.

The Microsoft::WRL::ComPtr class is a smart pointer implementation for COM objects.

💡
The class is part of the Windows Runtime API but it can be used in standard desktop applications as well.

Exceptions

An alternative way for handling failures is to throw an exception.

Using ComPtr ensures that the resources are properly released when an exception is thrown.

class HrException : public std::runtime_error
{
public:
    HrException(HRESULT hr) :
				std::runtime_error(HrToString(hr)),
				_hr(hr)
		{}

private:
    const HRESULT _hr;
};

inline void ThrowIfFailed(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw HrException(hr);
    }
}

Code

When calling QueryInterface, the __uuidof operator can be used for the riid parameter.

IDXGIDevice* dxgiDevice;

HRESULT hr = device->QueryInterface(
    __uuidof(IDXGIDevice),
    reinterpret_cast<void**>(&dxgiDevice));

if (SUCCEED(hr))
{
    // Use the interface
    ...

		dxgiDevice->Release();
}

The parameters can also be replaced by the IID_PPV_ARGS macro, and the SafeRelease function can be used without checking for the error code.

IDXGIDevice* dxgiDevice;

HRESULT hr = device->QueryInterface(IID_PPV_ARGS(&dxgiDevice));

if (SUCCEED(hr))
{
    // Use the interface
    ...
}

SafeRelease(&dxgiDevice);

The instance can be stored into a ComPtr smart pointer.

Microsoft::WRL::ComPtr<IDXGIDevice> dxgiDevice;

HRESULT hr = device->QueryInterface(IID_PPV_ARGS(&dxgiDevice));

if (SUCCEED(hr))
{
    // Use the interface
    ...
}

When using exceptions, there is no need to check for the HRESULT directly. The ComPtr class ensures that the reference is properly released.

Microsoft::WRL::ComPtr<IDXGIDevice> dxgiDevice;

ThrowIfFailed(
    device->QueryInterface(IID_PPV_ARGS(&dxgiDevice)
);

// Use the interface
...

Memory Allocation

COM is a binary standard and is not specific to a particular programming language. Therefore, COM does not use language-specific functionality for memory allocation.

COM define its own memory allocation functions to provide an abstraction layer over the heap allocator.

There are two functions:

Some COM functions allocate memory indirectly and the caller is responsible for calling CoTaskMemFree to free the memory.

Code

For example, the StringFromIID function returns the string representation of an interface identifier.

HRESULT StringFromIID(
  REFIID   rclsid,
  LPOLESTR *lplpsz
);

After calling StringFromIID, the variable specified as the lplpsz parameter must be freed by calling CoTaskMemFree.

std::wstring ToString(IID const& iid)
{
		wchar_t* iidString = nullptr;
		if (SUCCEEDED(StringFromIID(iid, &iidString)))
    {
				std::wstring value(iidString);
				CoTaskMemFree(iidString);
				return value;
		}
		return L"";
}

Glossary

WordDefinition
CLSIDClass Identifier
coclassCOM Class
COMComponent Object Model
GUIDGlobally Unique Identifier
IDLInterface Definition Language file
IIDInterface Identifier
MIDLMicrosoft Interface Definition Language
MTAMulti-Threaded Apartments
STASingle-Threaded Apartments
TLBType Library file
HRESULTResult Handle

References