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();
}
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:
I really hope this is helpful. There are some alternative COM handling tips and more explanations in this old but otherwise excellent blog post.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With