Binding C++ APIs, the COM way

on Monday, August 03, 2009

A couple of days ago, during a routine "aaagh, we still don't have a nice way to do C# bindings for C++ APIs" discussion, Miguel asked me how hard would it be to leverage COM to bind C++ APIs. I've been known to mess around with COM, as when I did Mono.WebBrowser/Gecko C# bindings, but I never did get around to do little test apps to try and streamline the whole process of using COM to bind a C++ API, so I jumped at the chance and got some interesting results.

COM, despite all the bad connotations surrounding it, is actually really simple: it is just a contract stating that any COM-conforming C++ class has at least 3 methods: QueryInterface, AddRef and Release. No matter how many members the class might have, those 3 are always present at the top of the class' vtable, so Mono's COM interop layer always knows where they are and can invoke them directly. And since the vtable layout for the class is known, any other method on that class can also be invoked in this way, bypassing name-mangling and other issues.

COM-comforming C++ classes can be described in C# via interfaces that have the same layout as the C++ class, so Mono knows exactly where the methods are in the vtable when invoking. Furthermore, COM support is pretty much transparent in C# - once you've defined your interfaces, you don't even realize you're using a COM object, it's just another object that you invoke methods on. Mono does all the marshalling for you, so you don't have to pass IntPtrs around, you just use the types you defined and everything will be marshalled for you behind the scenes.

Show me the code!

Let's say you have a little C++ library you'd like to use from C#:

class File {
public:
int Open();
int Close();
};

The C++ COM class

The first thing you need to do is create a COM class which will serve as a proxy between C# and your nice little library.

class COMFile {
public:
virtual int QueryInterface (void* id, void** result) {
*result = this;
return 0;
}
virtual int AddRef () { return 1; }
virtual int Release () { return 0; }

virtual int Open () { return file->Open(); }
virtual int Close () { return file->Close(); }

COMFile (File* f) : file(f) {}

private:
File* file;
};

All methods that need to be "exported" are marked as virtual, and the layout is what you would expect: the 3 methods on top that make this a COM class, plus the 2 methods that are proxying the calls to the library's File class.

AddRef and Release are standard refcounting methods - these will be called by Mono as needed when you invoke things that end up creating objects of this type. I'm just returning fixed values here, but it's important to note that when Release makes the refcount go to 0, the object should be released.

QueryInterface allows Mono's COM interop layer to figure out if a pointer can be cast to a specified type - via behind the scenes magic (and a little code), it enables a dynamic type system. This example is very simple and doesn't use inheritance, but with a complex binding you'll certainly have inheritance, and there is where QueryInterface comes in, for instance allowing for upcasts if your COM class inherits from several different classes.

You'll notice in the C# interface below that it is marked with a Guid - this id is unique to every class, and your C++ class definition should also have the same id. When QueryInterface is invoked, the id argument is the Guid of the type you want to cast to, so you can check if your C++ class is of the correct type by comparing ids, or if it is a subclass and you need to cast the result (or you don't support it at all, in which case you'd return null).

The C# interface

[Guid ("00000000-0000-0000-0000-000000001111")]
[InterfaceType (ComInterfaceType.InterfaceIsUnknown)]
[ComImport()]
public interface COMFile {
[PreserveSigAttribute]
[MethodImpl (MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
int Open();

[PreserveSigAttribute]
[MethodImpl (MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
int Close();
}

"Wait a minute... where are the 3 methods?", I can hear you thinking. Well, on the C# side you don't need them. The interface is marked ComImport(), so Mono already knows it's a COM class and it will add them for you, no questions asked. The C# interface only needs the definitions of the methods you want to access, and nothing else.

Putting it all together

Now that you have all your definitions in place, the only thing you need is to get a reference to a COMFile object. For this you're going to need to add a P/Invoke call to a C function on your proxy code that gives you a pointer to an instance of that class. You only need to do that for top level objects, because any objects that are returned via COM calls are directly available to you.

C++ proxy library

extern "C" {
COMFile* getptr() {
return new COMFile (new File ());
}
}

C# test app

[DllImport ("myglue")]
[return:MarshalAs(UnmanagedType.Interface)]
static extern COMFile getptr();

public static void Main() {
COMFile file = getptr();
int return = file.Open();
...
}

And there you go, the "file" variable is now talking to your COM class which is proxying all calls directly to your library. The glue code is very straightforward and can be easily autogenerated.

You can download a complete working sample here:
comtest-0.1.tar.gz
comtestsharp-0.1.tar.gz

Build and install both packages to the same prefix, then go to $prefix/lib/ and do " ln -s comtestsharp/* . ". Then just do"mono comtestsharp.exe" and you should see the output of the Open and Close calls.

Update: BTW, neither QI nor AddRef/Release are actually implemented properly in this little sample. The unused parameter "id" is the Guid that is getting requested, and QI should always check it against the current instance.