Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to manage object lifetime when working with COM interop?

I have a managed COM object written in C# and a native COM client and sink written in C++ (MFC and ATL). The client creates the object and advises to its event interface at startup, and unadvises from its event interface and releases the object at shutdown. The problem is that the COM object has a reference to the sink which is not released until garbage collection runs, at which point the client is already torn down and thus usually results in an access violation. It's probably not that big of a deal since the client is shutting down anyway, but I would like to resolve this gracefully if possible. I need my COM object to release my sink object in a more timely manner, and I don't really know where to start since my COM object doesn't work with the sink object explicitly.

My COM object:

public delegate void TestEventDelegate(int i);

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ITestObject
{
    int TestMethod();
    void InvokeTestEvent();
}

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ITestObjectEvents
{
    void TestEvent(int i);
}

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ITestObjectEvents))]
public class TestObject : ITestObject
{
    public event TestEventDelegate TestEvent;
    public TestObject() { }
    public int TestMethod()
    {
        return 42;
    }
    public void InvokeTestEvent()
    {
        if (TestEvent != null)
        {
            TestEvent(42);
        }
    }
}

The client is a standard MFC dialog-based program, with added support for ATL. My sink class:

class CTestObjectEventsSink : public CComObjectRootEx<CComSingleThreadModel>, public ITestObjectEvents
{
public:
    BEGIN_COM_MAP(CTestObjectEventsSink)
        COM_INTERFACE_ENTRY_IID(__uuidof(ITestObjectEvents), ITestObjectEvents)
    END_COM_MAP()
    HRESULT __stdcall raw_TestEvent(long i)
    {
        return S_OK;
    }
};

I have the following members in my dialog class:

ITestObjectPtr m_TestObject;
CComObject<CTestObjectEventsSink>* m_TestObjectEventsSink;
DWORD m_Cookie;

In OnInitDialog():

HRESULT hr = m_TestObject.CreateInstance(__uuidof(TestObject));
if(m_TestObject)
{
    hr = CComObject<CTestObjectEventsSink>::CreateInstance(&m_TestObjectEventsSink);
    if(SUCCEEDED(hr))
    {
        m_TestObjectEventsSink->AddRef(); // CComObject::CreateInstace() gives an object with a ref count of 0
        hr = AtlAdvise(m_TestObject, m_TestObjectEventsSink, __uuidof(ITestObjectEvents), &m_Cookie);
    }
}

In OnDestroy():

if(m_TestObject)
{
    HRESULT hr = AtlUnadvise(m_TestObject, __uuidof(ITestObjectEvents), m_Cookie);
    m_Cookie = 0;
    m_TestObjectEventsSink->Release();
    m_TestObjectEventsSink = NULL;
    m_TestObject.Release();
}
like image 306
Luke Avatar asked Jun 21 '13 19:06

Luke


1 Answers

First off, I'll just say that I've used your example code to implement a copy of what you have described, but I don't see any access violations when testing either Debug or Release builds.

So it is possible that there is some alternative explanation to what you're seeing (e.g. you may need to call Marshal.ReleaseCOMObject if you hold other interfaces to the native client).

There is a comprehensive description of when/when not to call ReleaseCOMObject on MSDN here.

Having said that, you're right that your C# COM object doesn't work with the COM client's sink object directly, but it does communicate with it through the C# event object. This allows you to implement a customised event object so you can trap the effect of the client's calls to AtlAdvise and AtlUnadvise.

For example, you can reimplement your event as follows (with some debugging output added):

private event TestEventDelegate _TestEvent;
public event TestEventDelegate TestEvent
{
    add
    {
        Debug.WriteLine("TRACE : TestObject.TestEventDelegate.add() called");
        _TestEvent += value;
    }
    remove
    {
        Debug.WriteLine("TRACE : TestObject.TestEventDelegate.remove() called");
        _TestEvent -= value;
    }
}

public void InvokeTestEvent()
{
    if (_TestEvent != null)
    {
        _TestEvent(42);
    }
}

To continue with the debugging output, you can add similar diagnostics to the MFC/ATL application and see exactly when the reference count is updated on the sink interface (note that this assumes Debug builds of both projects). So, for example, I added a Dump method to the sink implementation:

class CTestObjectEventsSink : public CComObjectRootEx<CComSingleThreadModel>, public ITestObjectEvents
{
public:
    BEGIN_COM_MAP(CTestObjectEventsSink)
        COM_INTERFACE_ENTRY_IID(__uuidof(ITestObjectEvents), ITestObjectEvents)
    END_COM_MAP()
    HRESULT __stdcall raw_TestEvent(long i)
    {
        return S_OK;
    }
    void Dump(LPCTSTR szMsg)
    {
        TRACE("TRACE : CTestObjectEventsSink::Dump() - m_dwRef = %u (%S)\n", m_dwRef, szMsg);
    }
};

Then, running the Debug client application through the IDE, you can see what's happening. First, during creation of the COM object:

HRESULT hr = m_TestObject.CreateInstance(__uuidof(TestObject));
if(m_TestObject)
{
    hr = CComObject<CTestObjectEventsSink>::CreateInstance(&m_TestObjectEventsSink);
    if(SUCCEEDED(hr))
    {
        m_TestObjectEventsSink->Dump(_T("after CreateInstance"));
        m_TestObjectEventsSink->AddRef(); // CComObject::CreateInstace() gives an object with a ref count of 0
        m_TestObjectEventsSink->Dump(_T("after AddRef"));
        hr = AtlAdvise(m_TestObject, m_TestObjectEventsSink, __uuidof(ITestObjectEvents), &m_Cookie);
        m_TestObjectEventsSink->Dump(_T("after AtlAdvise"));
    }
}

This gives the following debug output (you can see the C# trace from the AtlAdvise call in there)

TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 0 (after CreateInstance)
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 1 (after AddRef)
TRACE : TestObject.TestEventDelegate.add() called
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 2 (after AtlAdvise)

This looks as expected, we have a reference count of 2 - one from the native code AddRef and another (presumably) from AtlAdvise.

Now, you can check what happens if the InvokeTestEvent() method is called - here I do it twice:

m_TestObject->InvokeTestEvent();
m_TestObjectEventsSink->Dump(_T("after m_TestObject->InvokeTestEvent() first call"));
m_TestObject->InvokeTestEvent();
m_TestObjectEventsSink->Dump(_T("after m_TestObject->InvokeTestEvent() second call"));

This is the corresponding trace

TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() first call)   
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() second call) 

You can see that an additional AddRef has happened, the first time the event is fired. I'm guessing that this is the reference that doesn't get released until garbage collection.

Finally, in OnDestroy, we can see the reference count going down again. The code is

if(m_TestObject)
{
    m_TestObjectEventsSink->Dump(_T("before AtlUnadvise"));
    HRESULT hr = AtlUnadvise(m_TestObject, __uuidof(ITestObjectEvents), m_Cookie);
    m_TestObjectEventsSink->Dump(_T("after AtlUnadvise"));
    m_Cookie = 0;
    m_TestObjectEventsSink->Release();
    m_TestObjectEventsSink->Dump(_T("after Release"));
    m_TestObjectEventsSink = NULL;
    m_TestObject.Release();
}

and the trace output is

TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (before AtlUnadvise)
TRACE : TestObject.TestEventDelegate.remove() called
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after AtlUnadvise)
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 2 (after Release)

So you can see that AtlUnadvise doesn't affect the reference count (also noted by other people) but also note that we got a trace from the remove accessor of the C# COM object event, which is a possible location for forcing some garbage collection or other tear-down tasks.

To summarise:

  1. You reported access violations with the code you posted but I could not reproduce that error, so it is possible that the error you see is unrelated to the problem you described.
  2. You asked how you could interact with the COM client sink, and I have shown one potential way using a customised event implementation. This is supported with debug output showing how the two COM components interact.

I really hope this is helpful. There are some alternative COM handling tips and more explanations in this old but otherwise excellent blog post.

like image 95
Roger Rowland Avatar answered Jan 03 '23 13:01

Roger Rowland