2009-09-26

Inheritance doesn't really work in COM interop

Suppose you have the following two COM interfaces:

[ComImport, Guid("A37FBD41-5A69-11D3-8F84-00A0C9B4D50C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface ICorPublishProcessEnum {
    void Skip(int celt);
    void Reset();
    ICorPublishProcessEnum Clone();
    int GetCount();
    [PreserveSig]
    int Next(int celt, [Out, MarshalAs(UnmanagedType.LPArray)] ICorPublishProcess[] objects, out int pCeltFetched);
}
[ComImport, Guid("9F0C98F5-5A6A-11d3-8F84-00A0C9B4D50C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface ICorPublishAppDomainEnum {
    void Skip(int celt);
    void Reset();
    ICorPublishAppDomainEnum Clone();
    int GetCount();
    [PreserveSig]
    int Next(int celt, [Out, MarshalAs(UnmanagedType.LPArray)] ICorPublishAppDomain[] objects, out int pCeltFetched);
}

These may or may not look familiar to you, I'll talk about their purpose in a later post (though it's pretty easy to guess, or look up).

These interfaces look very much alike. So much alike, in fact, that we might be tempted to factor out their common bits. Ignore the .Clone() methods for now:

interface ICorPublishEnum<T> {
    void Skip(int celt);
    void Reset();
    ICorPublishAppDomainEnum Clone();
    int GetCount();
    [PreserveSig]
    int Next(int celt, [Out, MarshalAs(UnmanagedType.LPArray)] T[] objects, out int pCeltFetched);
}
 
[ComImport, Guid("9F0C98F5-5A6A-11d3-8F84-00A0C9B4D50C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface ICorPublishAppDomainEnum : ICorPublishEnum<ICorPublishAppDomain> { }

Now, first of all, to successfully interop with an IUnknown COM interface the methods must be declared in exactly the same order as in the IDL. This makes sense, as there's no other way to know how to call which method. This excludes naive uses of inheritance right off the bat, because even if it worked, methods would be inserted at the end instead of their proper position. But that's not a problem here: we make sure the order is the same by including all methods. So does this work? No.

You can retrieve an ICorPublishAppDomainEnum, but if you call any methods on it, you'll get an exception that QueryInterface() failed for the ICorPublishEnum interface. What's happening here is that the marshaler is only interested in the interface that originally defined the methods. The declarations we have supplied only mean that every object that implements ICorPublishAppDomainEnum is also required to implement ICorPublishEnum—which it won't, as we just made that interface up.

In case you were wondering about the use of generics: those also don't work. Suppose we redeclare the interface like this:

[ComImport, Guid("9F0C98F5-5A6A-11d3-8F84-00A0C9B4D50C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface ICorPublishAppDomainEnum {
    void Skip(int celt);
    void Reset();
    ICorPublishAppDomainEnum Clone();
    int GetCount();
    [PreserveSig]
    int Next<T>(int celt, [Out, MarshalAs(UnmanagedType.LPArray)] T[] objects, out int pCeltFetched);
}

This type cannot be JIT-compiled, and the first attempt to use it in any way will raise an EETypeLoadException. Almost nothing in interop marshaling is even aware of generics, and using them will cause all sorts of interesting low-level errors.

The moral of the story is not to get clever and use COM interfaces in the straightforward way they were intended to be used. More complicated scenarios where you need more flexibility can be handled in C++/CLI, where you can turn the reduced type safety into an advantage.

No comments: