Development Working Around TlbImp's Cleverness

TlbImp, the .Net tool to create Interop assemblies from COM type libraries, contains an optimization that presumably aims at making the consumption of the Interop assembly easier, but ultimately is a nuisance. Consider the following IDL code:

    import "oaidl.idl";
    import "ocidl.idl";
    
    [
      uuid( a657ef35-fea1-40ad-86d8-bb7b6085a0a3 ),
      version( 1.0 )
    ]
    library Test
    {
      
      [
        object,
        uuid( 84b2f017-b8fe-4c2c-87b8-0587b4bf5507 ),
        version( 1.0 ),
        oleautomation
      ]
      interface IFoo : IUnknown 
      {
        HRESULT Foo();
      }
    
      [
        object,
        uuid( 13d950d6-beb3-4dd3-957b-88b0e5eb5e3f ),
        version( 1.0 ),
        oleautomation
      ]
      interface IBar : IUnknown 
      {
        HRESULT CreateFoo( 
          [out, retval] IFoo **Foo
          );
      }
    
      [
        uuid( e01ea769-410c-4915-a48c-3522a8087a52 ),
        noncreatable 
      ]
      coclass Foo
      {
        interface IFoo;
      }
    
      [
        uuid( dca66832-fe3b-4658-a975-442b5678a9ec )
      ]
      coclass Bar
      {
        interface IBar;
      }
    }
    

Two things are worth noting about this IDL code: First, IBar::CreateFoo is declared to “return” an IFoo*, and second, there is only one coclass implementing IFoo, namely Foo. As a consequence, TlbImp attempts to be clever and assumes that if IBar::CreateFoo “returns” an IFoo*, it relly must be an Foo* that is returned. So in the resulting Interop assembly, the IBar interface will look as follows:

    [
      ComImport, InterfaceType((short) 1), 
      Guid("13D950D6-BEB3-4DD3-957B-88B0E5EB5E3F"), 
      TypeLibType((short) 0x100)
    ]
    public interface IBar
    {
      [return: MarshalAs(UnmanagedType.Interface)]
      [MethodImpl(MethodImplOptions.InternalCall, 
       MethodCodeType=MethodCodeType.Runtime)]
      Foo CreateFoo();
    }
    

Contrary to what the IDL defines, IBar::CreateFoo returns Foo, i.e. a concrete class.

First of all, the assumption underlying this optimization certainly is somewhat flaky as it is not quite in accord with the rules of COM – after all, the implementation of Bar is free to return whatever coclass implementing IFoo seems appropriate and is not limited to returning Foo coclass instances.

While this might not be a real problem in practice, the optimization has another an unpleasant effect on the testability of the library consuming the interface: To properly test this library, it might be a good idea to implement mock or stub implementations of IFoo and IBar. Unfortunately, due to the “optimized” return type of IBar::CreateFoo(), this turns out to be not quite easy, as the following code suggests:

    class FooStub : IFoo
    {
      public void Foo() 
      {}
    }
    
    class BarStub : IBar
    {
      public Foo CreateFoo()
      {
        // XXX: FooStub implements IFoo, but Foo is required!
        return new FooStub()
      }
    }
    

Workarounds

As pointless as TlbImp’s behavior in this regard might be, it is quite easy to work around this issue.

The first workaround is to define a dummy coclass in the IDL and declare it to implement IFoo as well:

    [
      uuid( ed93b3e6-104b-43d6-be34-972d7519bc62 )
    ]
    coclass Dummy
    {
      interface IFoo;
    }
    

Now that there are two candidate coclasses, TlbImp will not be able to infer the coclass from the interface and will not be able to apply its optimization. The drawback of this approach is, of course, that the additional Dummy coclass consitutes additional baggage in the IDL and the Interop assembly.

The second option is to forego declaring the interfaces of Foo in the coclass. These declarations mainly serve informative purposes and are not mandatory, so we can omit them. To satisfy MIDL’s requirement of naming at least one interface, we can just put in IUnknown:

    [
      uuid( e01ea769-410c-4915-a48c-3522a8087a52 ),
      noncreatable 
    ]
    coclass Foo
    {
      interface IUnknown;
    }
    

Again, TlbImp will not be able to apply its optimization and the resulting Interop assembly will contain a proper, mockable, interface IBar.

Needless to say, this approach has its own drawbacks. As a consequence of the missing interface implementation declarations, class Foo (in the Interop assembly) will not contain any methods and is basically useless – you are obliged to use the interface.

More importantly, however, the .Net class Foo will not implement IFoo – so although QueryInterface’ing IFoo from Foo would work, a statement like IBar bar = new BarClass() will now lead to a compiler error:

Cannot implicitly convert type 'Test.BarClass' to 'Test.IBar'. An explicit conversion exists (are you missing a cast?)

Although I consider the second option to be the cleaner approach, it is therefore best used for noncreatable coclasses only.

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home