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.

About these ads

2 Responses to “Working Around TlbImp’s Cleverness”


  1. 1 iLya Mukhanov June 2, 2009 at 7:21 pm

    Thanks for your article. It helped me very much. I had just the same situation when mock/stub test classes couldn’t be used because of that tlbimp @#$%^&* intelligence

    iLya [Elijah]

  2. 2 Klaus-Georg Adams January 24, 2011 at 11:18 am

    Hi Johannes,
    that was a problem that has bothered me a lot as well. Your solutions to this are very elegant.

    Thanks a lot, kga


Comments are currently closed.



Categories

Try Visual Assert, the unit testing add-in for Visual Studio (R)


NTrace: Function Boundary Tracing for Windows on IA-32

About me

Johannes Passing, M.Sc., living in Berlin, Germany.

Besides his consulting work, Johannes mainly focusses on Win32, COM, and NT kernel mode development, along with Java and .Net. He also is the author of cfix, a C/C++ unit testing framework for Win32 and NT kernel mode, Visual Assert, a Visual Studio Unit Testing-AddIn, and NTrace, a dynamic function boundary tracing toolkit for Windows NT/x86 kernel/user mode code.

Contact Johannes: jpassing (at) acm org

Johannes' GPG fingerprint is BBB1 1769 B82D CD07 D90A 57E8 9FE1 D441 F7A0 1BB1.

LinkedIn LinkedIn Profile
Xing Xing Profile
Twitter Follow me on Twitter (new)

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: