Mixing deterministic and non-deterministic cleanup

Mixing deterministic and non-deterministic cleanup

  • Comments 25

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(). 

 

When 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. 

 

Thanks,

Alan Chan

Visual C++ Libraries Team.


 

 

  • Hi Aled,

    Since the call stack is AV'ing at mscorwks.dll!SafeReleaseHelper(), it does look like that the .Net framework is calling Release().  

    Since it doesn't AV when you bypassed the IQuickActivate method, it looks like that there are some COM objects are still not being cleaned up properly.  Can you check the code in QuickActivate() (it should be managed) and make sure that ReleaseComObject() is called on all the COM pointers that were passed in through the QACONTAINER struct.  

    Even though the code might not use some of the COM pointers, the fact that there was a native->managed transistion, a RCW will be created for each COM pointer in the QACONTAINER.  Hence, ReleaseComObject() will still need to be called to ensure deterministic cleanup.

    Regarding to the questions to what to pass in ReleaseComObject(), you should be able to pass in the reference that came from the QACONTAINER.

    Thanks,

    Alan

  • Hi Alan,

    Thanks for the info. It has helped greatly in understanding the problem I am seeing and finding a workaround!

    The code in QuickActivate() in MFC is unmanaged (it is in COleControlSite), but you're right that COM objects passed in QACONTAINER weren't having their RCWs cleaned up. I couldn't see any code in the MFC support classes that would do that cleanup so I manually got the COM pointers from the COleControlSite and did a ReleaseComObject() on them when my CWinFormsDialog/View based class was destroyed.

    Thanks,

    Aled.

  • Hi, My name is Andreea Isac and I’ve been a member of the Visual C++ Libraries team for one year and

  • Hi Aled/Alan,

    Do you have any sample workarounds? I've run into the same situation and am trying to figure out how to call ReleaseComObject (which is a managed function) in releasing (COleControlSite ) COM Pointers from the unmanaged side.  I'm using a CWinFormsControl an am also seeing an AV within mscorwks.dll

    Thanks,

    Ray

  • Hi Alan,

    Would these known issues by any chance be fixed in VS2005 SP1?

    Thanks,

  • Hi Alan/Aled

    Is it m_xOleClientSite, m_xPropertyNotifySink and m_xEventSink, which should be released from the unmanaged site?

    Thanks,

  • Yes, this should have been fixed in SP1.

    Thanks,

    Alan

  • Hi Alan

    We unfortunately still have the problem with sp1, so it doesn't seem to have been fixed.

    Did you have some code showing how to do it manually?

    regards,

  • Jørn,

    I just happened to stumble on this blog entry again today and noticed your postings.

    For info, this is how I overcame this random crashing problem:

    I created a template class that inherited from CWinFormsDialog. In this, I added an OnDestroy() function as follows:

    void CWinFormsView2::OnDestroy()

    {

    COleControlSite* pSite = GetControlControlSite();

    if (pSite != NULL) {

    // Release pointers passed into QACONTAINER.

    //

    IUnknown* pUnk1 = (IUnknown*)(&(pSite->m_xOleClientSite));

    IUnknown* pUnk2 = (IUnknown*)(&(pSite->m_xPropertyNotifySink));

    IUnknown* pUnk3 = (IUnknown*)(&(pSite->m_xEventSink));

    System::IntPtr pUnknAsInt;

    System::Object^ oManagedWrapper;

    if (pUnk1 != NULL) {

    pUnknAsInt = static_cast<System::IntPtr>(pUnk1);

    oManagedWrapper = System::Runtime::InteropServices::Marshal::GetObjectForIUnknown(pUnknAsInt);

    System::Runtime::InteropServices::Marshal::FinalReleaseComObject(oManagedWrapper);

    }

    // ...Repeat for pUnk2 & pUnk3...

    }

    }

    I've not tried removing it to see if SP1 has fixed the problem (as things work OK so I leave well alone!)

    Cheers,

    aled.

  • I think this problem is fixed in Orcas?

Page 2 of 2 (25 items) 12