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:
public interface AppEvents
void SheetSelectionChange(object Sh, Range Target);
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:
- It is implemented using unsafe managed code
- It responds to QIs for the above GUID using the new ICustomQueryInterface which allows managed objects to customize their IUnknown.QueryInterface behavior
- 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.
- 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
- 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”
- 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