Mixing deterministic and non-deterministic cleanup
Hi, my name is Alan Chan. I’m a software design engineer in Visual C++ libraries team. As the name suggests, my team owns some of the C/C++ libraries such as ATL, MFC, CRT, STL, and OpenMP. Today, I’m going to talk about one very interesting COM interop bug that I have worked on recently.
Basic scenario: We have a mixed MFC and .NET application. When the user presses a button in the application, it pops up a MFC dialog which hosts a managed control. This managed control implements IQuickActivate and is activated through COleControlSite::QuickActivate(). This function creates all the appropriate sinks – in this case IAdviseSinkEx and IPropertyNotifySink – and passes them to the managed control through IQuickActivate::QuickActivate().
hen the user dismisses the dialog, the destructor of COleControlSite is called. The destructor disconnects all the sink COM objects that were passed in through QuickActivate(), deactivates the managed control, and deletes the sink COM objects.
This works on the whole. However, if the user opens and closes the dialog repeatedly, the application will AV and crash. If you read the title, you can probably guess what’s happening! It turns out that the garbage collector (GC) is calling IUnknown::Release() on the IAdviseSinkEx and IPropertyNotifySink COM objects after they have been deleted.
As most of you know already, whenever a COM object is passed from native code to managed code, the .Net framework creates Runtime-Callable Wrappers (RCW) automatically. The RCW wraps the object and controls lifetime. In this case, when the destructor of COleControlSite calls Unadvise() to disconnect the sinks, the managed control does, in fact, disconnect the sinks and drop all references to the RCWs of the sink objects. However, IUnknown::Release() is not called at this point. IUnknown::Release() is called in the RCW’s finalizer function. Dropping all references to the RCWs only means that the RCWs are eligible for garbage collection. The GC can collect these RCWs at anytime. Garbage collection is determined by memory pressure. If these RCWs were garbage collected and finalized after the actual sink COM objects have been deleted, it will cause an AV.
So how do we fix this? We need some way to clean up these RCWs deterministically. Sadly, RCW doesn’t implement IDisposable interface or else we could call IDisposable::Dispose() for cleanup. Fortunately, the framework provides a function, Marshal.ReleaseComObject(), for deterministic cleanup. Each RCW keeps a reference count of how many managed references are pointing to itself. (This is completely separated from the reference count in the COM object.) Every time you copy or QueryInterface() a managed COM reference, the RCW automatically increments its reference count. When ReleaseComObject() is called, the RCW decrements its reference count. As soon as the reference count reaches zero, the RCW will then call the finalizer function and release all the COM reference counts that it is holding deterministically.
Therefore, similar to calling IUnknown::Release() in native code, we should always call ReleaseComObject() on the managed COM references before it goes out of scope. This should guarantee that IUnknown::Release() is called deterministically.
I hope this will save you time when debugging COM interop bugs.
Visual C++ Libraries Team.