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