Archive for the '.Net' Category

RCW Reference Counting Rules != COM Reference Counting Rules

Avoiding COM object leaks in managed applications that make use of COM Interop can be a daunting task. While diligent tracking of COM object references and appropriate usage of Marshal.ReleaseComObject usually works fine, COM Interop is always good for surprises.

Recently having been tracking down a COM object leak in a COM/.Net-Interop-centric application, I noticed that the CLR did not quite manage the reference count on my COM object as I expected it to do — more precisely, it incremented the referece count of a COM object when it was passed (from COM) as a method parameter to a callback implemented in .Net — which, of course, contradicts the rules of COM. So while RCWs indeed mostly follow the rules of COM reference counting, they obviously do not do follow the rules in their entirety. Once I spotted this difference, it was easy to find an explanation of this very topic by Ian Griffiths, which is worth quoting [reformatted by me]:

[…]
And by the way, the reference counting is kind of similarish to COM, in that, as you point out, things get addrefed when they are passed to you. But they’re actually not the same. Consider this C# class that
implements a COM interface:

public class Foo : ISomeComInterface
{
  public void Spong(ISomeOtherComInterface bar)
  {
    bar.Quux();
  }
}

Suppose that Spong is the only member of ISomeComInterface. (Other than the basic IUnknown members, obviously.) This Spong method is passed another COM interface as a parameter. And let’s suppose that some non-.NET client is going to call this Spong method on our .NET object via COM interop.

The reference counting rules for COM are not the same as those for the RCW in this case.

For COM, the rule here is that the interface is AddRefed for you before it gets passed in, and is Released for you after you return. In other words, you are not required to do any AddRefing or Releasing on a COM object passed to you in this way *unless* you want to keep hold of a reference to it after the call returns. In that case you would AddRef it.

Compare this with the RCW reference count. As with COM, the RCW’s reference count will be incremented for you when the parameter is passed in. But unlike in COM, it won’t be decremented for you automatically when you return.

You could sum up the difference like this:

  • COM assumes you won’t be holding onto the object reference when the method returns
  • The RCW assumes you *will* be holding onto the object reference when the method returns.

So if you don’t plan to keep hold of the object reference, then the method should really look like this:

public void Spong(ISomeOtherComInterface bar)
{
  bar.Quux();
  Marshal.ReleaseComObject(bar);
}

According to the COM rules of reference counting, this would be a programming error. But with RCWs, it’s how you tell the system you’re not holding onto the object after the method returns.

Pretty counter-intuitive… Plus, I am not aware of any official documentation on this topic.

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.


Categories




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 Profile
Xing Profile
Github Profile