COM Interface Basics
COM is many things, but at its core, it’s all about consuming and exposing functionality using
C++ virtual function tables derived from the IUnknown
struct.
IUnknown
{
public:
virtual HRESULT __stdcall QueryInterface( const GUID& riid, void **ppvObject) = 0;
virtual unsigned long __stdcall AddRef( ) = 0;
virtual unsigned long __stdcall Release( ) = 0;
};
IUnknown
can easily be implemented in C:
typedef struct IUnknownVtbl
{
HRESULT ( __stdcall *QueryInterface )( IUnknown * This,
const GUID* riid, void **ppvObject);
unsigned long ( __stdcall *AddRef )( IUnknown * This );
unsigned long ( __stdcall *Release )( IUnknown * This );
} IUnknownVtbl;
typedef struct tagIUnknown
{
struct IUnknownVtbl *lpVtbl;
}IUnknown;
It’s just easier to write in C++, but a the end of the day, it’s just a definition of a structure containing pointers to functions.
With COM the calling app is responsible for releasing resources back to the implementation,
and every instance of a COM interface must be released using the Release
member function
of the virtual function table. COM objects are reference counted, or at least they are expected
to behave as if they are. Calling Release
decrements the reference count of the object, and destroys
the object when the reference count becomes 0
.
AddRef
increments the reference count of the implementation, and apps should usually call AddRef
when they
make copies of the interface pointer.
COM object implements one, or more interfaces, and the role of QueryInterface
is to provide
access to the interfaces implemented by the object. Interfaces are identified by a GUID
,
which is a 16 byte structure.
All COM objects supports the IUnknown
interface, and a query for the IUnknown
interface on any of the
object’s interfaces must always return the same pointer value. This allows clients to determine
if two pointers point to the same object by calling QueryInterface
with IID_IUnknown
and comparing
the results. Pointers to other interfaces do not have this restriction.
There are three requirements for the implementations of QueryInterface
:
- The set of interfaces that can be accessed using
QueryInterface
must be static. If a call toQueryInterface
for a pointer to an interface succeeds the first time, then it must succeed again, and when it fails the first time, it must fail on all subsequent calls. - If a client calls
QueryInterface
successfully for another interface implemented by an object, then a query through the obtained interface for the original interface must succeed. - If a client calls
QueryInterface
successfully for a second interface, and callsQueryInterface
successfully for a third interface, then a call toQueryInterface
for the initial interface through the third interface must succeed.
Error handling in COM
Almost all COM functions and interface methods return a value of the type HRESULT. The HRESULT is a way of returning a success, warning, or error value.
According to the COM specification, a result of zero indicates success, and a nonzero result indicates failure. Unfortunately this is not always true, and even some core COM interfaces does not follow this rule. When this is the case, it’s usually documented.
The COM API provides two macros that is commonly used to detect if an operation
succeeded or failed, appropriately called SUCCEEDED
and FAILED
.
IXMLDOMDocument3* ptr = ...
IXMLDOMNode* node = nullptr;
auto hr = ptr->QueryInterface( __uuidof( IXMLDOMNode ), ( void** )&node );
if ( FAILED( hr ) )
{
QueryInterface
failed, perform error handling. Do not call node->Release( )
, or any other
function of the IXMLDOMNode
interface since node
is not a valid interface pointer.
}
else
{
// QueryInterface succeeded, and node points to an IXMLDOMNode interface
BSTR prefix = nullptr;
hr = node->get_prefix( &prefix );
if ( SUCCEEDED( hr ) )
{
wprintf( L"Prefix:%s\n", prefix );
Caller is responsible for freeing prefix
using SysFreeString
:
SysFreeString( prefix );
}
node->Release( );
}
ptr->Release( );