Better eventing support in CLR 4.0 using NOPIA support

Events implementation in the Interop Assemblies does have its shortcomings. I enumerated the issues in “COM Interop: Handling events has side effects” post. The quick recap is that Interop Assemblies eventing support does create “ghost RCWs” that get in the way of deterministically managing the life-time of your COM objects.

But there is a hope! The Type Embedding work that we are doing for NOPIA feature eliminates the dependency on Interop Assemblies by embedding the partial local copies of Interop interfaces into the assembly that uses those interfaces.

This customized compiler behavior for Interop Types has been a great opportunity for us to fix the way COM events are handled. Since the implementation of the event sink is currently located in the Interop Assemblies in the pure IL – and the design decision behind types embedding is that we never copy IL – we needed to come up with a different mechanism.

So, we taught the compiler to recognize code patterns that subscribe to events for Interop Assembly types and replace this pattern with something else. To give you an example from my PDC Type Embedding demo – here is the code that subscribes to SheetSelectionChange event from Excel’s Applcation object.

 xlapp.SheetSelectionChange += xlapp_SheetSelectionChange;

The code that is emitted by the compiler is very different though and it looks like this (I copied the below line from Reflector’s Disassembler window):

ComEventsHelper.Combine(xlapp, new Guid("00024413-0000-0000-C000-000000000046"), 0x616, new AppEvents_SheetSelectionChangeEventHandler(Program.xlapp_SheetSelectionChange));

You can probably figure out some of the parameters to ComEventsHelper.Combine API i.e. xlapp and the delegate are self-describing. But the reason for the existence of the Guid and the mysterious integer 0x616 may not be immediately apparent. So, what is going on here?

When compilers encounter code that subscribes to an event on an interface coming from Interop Assembly they are trying to analyze how this event is exposed by the COM object. In particular, they need to find the interface COM object would call on when raising the event. So they look in the PIA and end up finding the AppEvents interface which is declared like this:

[Guid("00024413-0000-0000-C000-000000000046")]
public interface AppEvents
{
    [DispId(2612)]
    void AfterCalculate();
    [DispId(1558)]
    void SheetSelectionChange(object Sh, Range Target);
    [DispId(1556)]
    void WindowActivate(Workbook Wb, Window Wn);

….
}

Look at the Guid on this interface – this is the same Guid passed to ComEventsHelper.Combine API.

Also, if you have no problem translating between decimal and hexadecimal you have already realized that 1558=0x616 (I personally used calc.exe to do translation).

So, Combine API attaches an event sink to the COM object’s connection point that handles invocation on the interface with specified GUID. It also tells the event sink that when a method with dispid==0x616 is called it should invoke the delegate that is passed as the last parameter.

Important thing about the event sink is:

  1. It is implemented using unsafe managed code
  2. It responds to QIs for the above GUID  using the new ICustomQueryInterface which allows managed objects to customize their IUnknown.QueryInterface behavior
  3. It only handles late-bound invocation on the source interface. Generally, this should not be a big problem since almost all the COM applications use late-binding when raising events.
  4. Subsequent calls to ComEventsHelper.Combine for the same COM object will re-use the existing sink – resulting that there always is only one interop transition when managed code registers multiple event handles
  5. The event sink does not marshal parameters to the call if there is no user delegate register against a particular dispid. This solves the problem of the “ghost RCWs”
  6. There is a corresponding ComEventHelpers.Remove API which allows to remove the event handler

So, effectively if you compile your code with NOPIAs – the problems I previously raised with the way events are handled by Interop Assemblies should go away!

I am also attaching the source of the simple demo that demonstrates Type Equivalence. You will need to download the Dev10 CTP here to run it - https://go.microsoft.com/fwlink/?LinkId=129231

TypeEmbeddingDemo.zip

Comments

  • Anonymous
    October 28, 2008
    PingBack from http://mstechnews.info/2008/10/better-eventing-support-in-clr-40-using-nopia-support/

  • Anonymous
    October 29, 2008
    Welcome to my article This article was inspired by some work carried out by my good friend Misha at http://blogs.msdn.com/mshneer/archive/2008/10/28/better-eventing-support-in-clr-4-0-using-nopia-support.aspx

  • Anonymous
    November 04, 2008
    Misha has some great content about the advances in COM Interop in .NET 4.0. Check it out: http://blogs.msdn.com/mshneer/archive/2008/10/28/better-eventing-support-in-clr-4-0-using-nopia-support.aspx

  • Anonymous
    February 17, 2009
    Misha Shneerson, a senior developer on the VSTO team, has a great post giving us hope in the next version

  • Anonymous
    October 12, 2009
    wht is NOPIA support ?  Please provide information over it. Provide links to related topics if possible.

  • Anonymous
    April 23, 2011
    Ok, I'm late to this party, but wow, this is HUGE. Thanks so much for this, the previous method of safely sinking the event was extremely awkward. I can go sleep better now... :-)

  • Anonymous
    June 23, 2011
    Take two - being more careful how I touch the keyboard as it appears I did something that posted accidentally: There is another way I believe will create a ghost RCW: dim x as object dim y as int x = mycomApp.object1 y = object1.object2.y I believe I just created an RCW for object2 in order to get to some data in object2. Or: y = object1.somecollectionofcomobjects.Item(1).y Did I not just create two ghost RCWs - one for the collection object and one for one of the objects in the collection. Again, a Dispose method and judicious use of Dispose by the runtime will cause the runtime to release the actual COM objects in my application in a timely manner.