使用COM互操作时如何管理对象的生命周期?

Luk*_*uke 7 com com-interop

我有一个用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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

客户端是标准的基于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;
    }
};
Run Code Online (Sandbox Code Playgroud)

我在对话框类中有以下成员:

ITestObjectPtr m_TestObject;
CComObject<CTestObjectEventsSink>* m_TestObjectEventsSink;
DWORD m_Cookie;
Run Code Online (Sandbox Code Playgroud)

在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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在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();
}
Run Code Online (Sandbox Code Playgroud)

Rog*_*and 3

首先,我只想说我已经使用您的示例代码来实现您所描述的内容的副本,但在测试调试或发布版本时我没有看到任何访问冲突。

因此,您所看到的内容可能有一些替代解释(例如,Marshal.ReleaseCOMObject如果您持有本机客户端的其他接口,您可能需要调用)。

MSDN 上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);
    }
}
Run Code Online (Sandbox Code Playgroud)

要继续调试输出,您可以向 MFC/ATL 应用程序添加类似的诊断,并准确查看接收器接口上的引用计数何时更新(请注意,这假定两个项目的调试版本)。因此,例如,我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);
    }
};
Run Code Online (Sandbox Code Playgroud)

然后,通过 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"));
    }
}
Run Code Online (Sandbox Code Playgroud)

这给出了以下调试输出(您可以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"));
Run Code Online (Sandbox Code Playgroud)

这是对应的轨迹

TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() first call)   
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() second call) 
Run Code Online (Sandbox Code Playgroud)

AddRef您可以看到第一次触发事件时发生了额外的事情。我猜测这是在垃圾收集之前不会释放的引用。

最后,在 中OnDestroy,我们可以看到引用计数再次下降。代码是

TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() first call)   
TRACE : CTestObjectEventsSink::Dump() - m_dwRef = 3 (after m_TestObject->InvokeTestEvent() second call) 
Run Code Online (Sandbox Code Playgroud)

跟踪输出是

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不会影响引用计数(其他人也注意到了remove),但还要注意我们从C# COM 对象事件的访问器获得了跟踪,这是强制执行某些垃圾收集或其他撕裂的可能位置-向下任务。

总结一下:

  1. 您报告了您发布的代码的访问冲突,但我无法重现该错误,因此您看到的错误可能与您描述的问题无关。
  2. 您询问如何与 COM 客户端接收器交互,我已经展示了一种使用自定义事件实现的潜在方法。调试输出支持这一点,显示两个 COM 组件如何交互。

我真的希望这有帮助。在这篇旧的但在其他方面很优秀的博客文章中,有一些替代的 COM 处理技巧和更多解释。