我有一个用C#编写的托管COM对象和一个用C ++(MFC和ATL)编写的本机COM客户端和接收器。客户端在启动时创建对象并建议其事件接口,并从其事件接口取消设置并在关闭时释放对象。问题是COM对象具有对接收器的引用,该接收器在垃圾收集运行之前不会被释放,此时客户端已经被拆除,因此通常会导致访问冲突。这可能不是一件大事,因为无论如何客户端都在关闭,但我想尽可能优雅地解决这个问题。我需要我的COM对象以更及时的方式释放我的接收器对象,我真的不知道从哪里开始,因为我的COM对象没有明确地使用接收器对象。
我的COM对象:
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);
}
}
}
客户端是标准的基于MFC对话框的程序,增加了对ATL的支持。我的下课:
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;
}
};
我的对话框类中有以下成员:
ITestObjectPtr m_TestObject;
CComObject<CTestObjectEventsSink>* m_TestObjectEventsSink;
DWORD m_Cookie;
在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);
}
}
在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();
}
答案 0 :(得分:3)
首先,我只是说我已经使用您的示例代码来实现您所描述的内容的副本,但是在测试Debug或Release版本时我没有看到任何访问冲突。
因此有可能对您所看到的内容有一些替代解释(例如,如果您将其他接口保存到本机客户端,则可能需要调用Marshal.ReleaseCOMObject
。)
有关何时/何时不在MSDN here致电ReleaseCOMObject
的详细说明。
话虽如此,你的C#COM对象不适用于COM客户端的接收器对象直接,但它 通过它与进行通信是正确的。 C#事件对象。这允许您实现自定义事件对象,以便捕获客户端对AtlAdvise
和AtlUnadvise
的调用的影响。
例如,您可以按如下方式重新实现事件(添加一些调试输出):
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);
}
}
要继续调试输出,您可以向MFC / ATL应用程序添加类似的诊断,并确切了解在接收器接口上更新引用计数的时间(请注意,这假定Debug 的构建项目)。因此,例如,我向接收器实现添加了Dump
方法:
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);
}
};
然后,通过IDE运行Debug客户端应用程序,您可以看到发生了什么。首先,在创建COM对象期间:
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"));
}
}
这提供了以下调试输出(您可以在那里看到来自AtlAdvise
调用的C#跟踪)
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)
这看起来符合预期,我们的引用计数为2 - 来自本机代码AddRef
,另一个(大概)来自AtlAdvise
。
现在,您可以检查调用InvokeTestEvent()
方法后会发生什么 - 这里我做了两次:
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"));
这是相应的痕迹
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() first call)
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() second call)
您可以看到第一次触发事件时发生了额外的AddRef
。我猜这是在垃圾收集之前不会被释放的引用。
最后,在OnDestroy
中,我们可以看到引用计数再次下降。代码是
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();
}
并且跟踪输出是
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)
因此,您可以看到AtlUnadvise
不会影响引用计数(also noted by other people),但请注意我们从C#COM对象事件的remove
访问器中获取了跟踪,这是强制进行一些垃圾收集或其他拆除任务的可能位置。
总结:
我真的希望这会有所帮助。在this old but otherwise excellent blog post中有一些替代的COM处理技巧和更多解释。