[ros-dev] Kernel-mode COM required for Kernel Streaming (ks.sys)

KJKHyperion hackbunny at reactos.com
Mon Sep 12 02:59:01 CEST 2005


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


More information about the Ros-dev mailing list