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:
- Windows Runtime (WinRT)
- Windows Imaging
- DirectX
- Direct3D
- Direct2D
- DirectWrite
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
- Enables inter-process communication in distinct languages.
- Provides a stable application binary interface (ABI).
- Enables well-defined interfaces that are separated from the implementation.
- Memory management through reference counting.
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.
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.
- When the object is first created, its reference count is 1.
- When the reference to an object is duplicated, the reference count is incremented by one.
- When a reference is released, the reference count is decremented by one.
- When the reference count of the object reaches zero, the object deletes itself.
Threading Model
In COM, the threading model is handled through the concept of apartments.
COM objects are divided into groups called apartments.
- A COM object is owned by a single apartment.
- An apartment can be either single-threaded (STA), or multi-threaded (MTA).
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
);
pvReserved
is always null.
dwCoInit
is set to aCOINIT
enumeration value.
There are two common values for the dwCoInit
parameter.
COINIT_APARTMENTTHREADED
– Single-threaded Apartment (slower).
COINITBASE_MULTITHREADED
– Multi-threaded Apartment (recommended).
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
.
- The
SUCCEEDED
macro returnsTRUE
if anHRESULT
is a success code, andFALSE
if it is an error code.
- The
FAILED
macro tests for failure.
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
);
rclsid
is the CLSID associated with the class to create.
pUnkOuter
is set to null (see the COM documentation for more information).
dwClsContext
is a flag that indicates whether the object runs in the same process as the application.
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
.
riid
is the IID associated with the interface to use.
ppv
receives a pointer to the interface.
Code
This sample code creates a COM object that implements the IWICImagingFactory
interface from the Windows Imaging API.
- The CLSID is
CLSID_WICImagingFactory2
.
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:
QueryInterface
enables a program to query the capabilities of the object.
AddRef
increments the reference count on an object.
Release
decrements the reference count on an object.
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.
riid
is the IID associated with the interface to use.
ppvObject
receives a pointer to the interface.
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.
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:
CoTaskMemAlloc
allocates a block of memory.
CoTaskMemFree
frees a block of memory that was allocated withCoTaskMemAlloc
.
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
);
rclsid
is the CLSID of interface identifier to be converted.
lplpsz
is a pointer to a variable that receives the resulting string.
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
Word | Definition |
---|---|
CLSID | Class Identifier |
coclass | COM Class |
COM | Component Object Model |
GUID | Globally Unique Identifier |
IDL | Interface Definition Language file |
IID | Interface Identifier |
MIDL | Microsoft Interface Definition Language |
MTA | Multi-Threaded Apartments |
STA | Single-Threaded Apartments |
TLB | Type Library file |
HRESULT | Result Handle |
References
- Component Object Model (COM), Microsoft
- Component Object Model, Wikipedia