Andrew "Silver Blade" Greenwood wrote:
And I have *no* idea how to use COM. So far it looks
really really ugly.
don't let COM scare you, it's just pretty dressing around arrays of
function pointers. Quick crash course on COM...
== Co-classes and interfaces ==
An interface is a list of methods supported by an object, easy as that.
The binary format to embed an interface in an object is very simple: an
interface is a pointer to a so-called virtual table (v-table), which in
turn is simply an array (actually a structure, because they
realistically have different prototypes) of function pointers. Here's
the most famous COM interface, IUnknown, in C syntax:
struct IUnknown
{
struct IUnknown_Vtbl * lpVtbl;
};
struct IUnknown_Vtbl
{
HRESULT (* QueryInterface)(struct IUnknown * lpThis, REFIID iid,
void ** ppvObject);
ULONG (* AddRef)(struct IUnknown * lpThis);
ULONG (* Release)(struct IUnknown * lpThis);
};
All interfaces are identified uniquely by an UUID, which in this context
is referred to as an IID
A co-class is an implementation (literally, a bunch of code) of a
certain set of interfaces. Co-classes are identified uniquely, too, and
their identifiers are known as CLSIDs
== How to implement co-classes in C ==
First of all, let's remind ourselves that an "object" is really just
allocated data pointed to by a typed pointer (or even an opaque
non-pointer, like a handle). To make a practical example, I'll show you
a way to implement ISequentialStream, probably the simplest interface
that actually serves a purpose on its own. First, we declare the co-class:
struct MyStream
{
// COM-visible
union
{
IUnknown IUnknown;
ISequentialStream ISequentialStream;
};
// private
LONG refCount;
HANDLE hFile;
};
I could have used C-style struct inheritance, which is cleaner than
using an union but hides too many details. Why the union? It will be
clearer when we'll implement IUnknown::QueryInterface. For know, it
probably helps to know that ISequentialStream contains all the function
pointers that IUnknown does (all COM interfaces are required to), plus
two more on its own, so the use of an union is somewhat safe
How are objects of type struct MyStream created? COM doesn't really
care, and it doesn't specify an unified way to allocate, instantiate and
initialize an object. In many cases ("push" model) you can simply pass
around interfaces, and nobody will ask where do they come from. In the
frequent cases in which instead you have to *ask* for an object ("pull"
model), a class factory is used. A class factory (in general) is an
object that does nothing else than creating objects of a certain class.
The general outline is this:
- client needs an implementation of ISomething. The configuration says
to use the implementation provided by the co-class with id {such-and-such}
- client calls COM asking for an instance of co-class {such-and-such}
and a pointer to its ISomething
- the COM database says {such-and-such}is implemented in blah.dll. COM
loads blah.dll, and asks it (doesn't matter how for now, it's documented
anyway) to create a class factory for {such-and-such} (the class
factory, in turn, is an implementation of IClassFactory)
- COM tells the class factory to create an object of type {such-and-such}
- with the object in hand, COM asks the object for its ISomething
- if everything is ok, COM returns the ISomething pointer to the client
(note: "instance of co-class {such-and-such}" and "object of type
{such-and-such}" are just two ways of saying the same thing)
For the sake of simplicity, let's assume a "push" model, where we create
the object ourselves in an unspecified way and pass pointers to its
interfaces around. Here's how we allocate, instantiate and initialize an
object of type struct MyStream:
// allocation
struct MyStream * obj = malloc(sizeof(struct MyStream));
// instantiation
obj->ISequentialStream.lpVtbl = MyStream_ISequentialStream_Vtbl;
obj->refCount = 1;
// initialization
obj->hFile = CreateFile(...);
As for MyStream_ISequentialStream_Vtbl, it sounds menacing but it's
quite easy:
static const ISequentialStream MyStream_ISequentialStream_Vtbl =
{
// IUnknown methods
MyStream_QueryInterface,
MyStream_AddRef,
MyStream_Release,
// ISequentialStream methods
MyStream_Read,
MyStream_Write
};
QueryInterface, AddRef and Release are merely tedious, but they are
implemented with boilerplate code. Here's a quite typical implementation:
// This is how our clients can ask if we implement an interface
HRESULT MyStream_QueryInterface(struct ISequentialStream * lpThis,
REFIID iid, void ** ppvObject)
{
// access our private data. Note that thanks to CONTAINING_RECORD
interfaces could be anywhere
// in an object, not necessarily at the beginning
struct MyStream * This = CONTAINING_RECORD(lpThis, struct MyStream,
ISequentialStream);
*ppvObject = NULL;
if(IsEqualIID(iid, &IID_IUnknown))
*ppvObject = &This->IUnknown;
else if(IsEqualIID(iid, &IID_ISequentialStream))
*ppvObject = &This->ISequentialStream;
else
return E_NOINTERFACE;
return S_OK:
}
// This is how we're told the object is in use
ULONG AddRef(struct ISequentialStream * lpThis)
{
struct MyStream * This = CONTAINING_RECORD(lpThis, struct MyStream,
ISequentialStream);
return InterlockedIncrement(&This->refCount);
}
// And this is how we're told our services are no longer required
ULONG Release(struct ISequentialStream * lpThis)
{
struct MyStream * This = CONTAINING_RECORD(lpThis, struct MyStream,
ISequentialStream);
ULONG ul = InterlockedDecrement(&This->refCount);
// no pointers to this object exist anywhere anymore: we can destroy
and free it
if(ul == 0)
{
// destroy
CloseHandle(This->hFile);
// free
free(This);
// decrement the global count of objects
InterlockedDecrement(&numObjects);
}
return ul;
}
No, seriously, just that. That's it. Note we aren't required to actually
keep a reference count, since nothing in COM forbids to implement
objects as singletons (i.e. a single instance for all clients), and
indeed in many common cases we can simplify things bynot using
malloc/free and instead passing around every time the same
statically-allocated interface.
(as for the other functions, well, they don't really matter.
MyStream_Read calls ReadFile on This->hFile and MyStream_Write calls
WriteFile)
And this is all you absolutely need to know about COM! if you need more,
just ask