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

我有一个用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();
}

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

所以有可能对你所看到的有一些另外的解释(例如,如果你持有与本地客户端的其他接口,你可能需要调用Marshal.ReleaseCOMObject )。

有关何时/何时不在MSDN上调用ReleaseCOMObject的全面描述。

话虽如此,您说的正确的是您的C#COM对象不直接与COM客户端的接收器对象一起工作,但它通过C#事件对象与它进行通信。 这允许您实现自定义的事件对象,以便您可以将客户端调用的效果捕获到AtlAdviseAtlUnadvise

例如,你可以重新实现你的事件如下(添加了一些调试输出):

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看到来自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)

这看起来和预期的一样,我们有一个来自本地代码AddRef的引用计数2和一个来自AtlAdvise推测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不影响引用计数(也被其他人注意到),但是也注意到我们从C#COM对象事件的remove存取器获得了一个跟踪,这是一个强制某些垃圾收集的可能位置或其他拆卸任务。

总结:

  • 您使用您发布的代码报告了访问违规,但我无法重现该错误,因此您看到的错误可能与您描述的问题无关。
  • 您问过您如何与COM客户端接收器进行交互,并且我已经展示了一种使用自定义事件实现的潜在方式。 调试输出支持这两个COM组件如何交互。
  • 我真的希望这是有帮助的。 还有一些替代COM处理技巧和更多的解释,在这个旧的,但另有优秀的博客文章。

    链接地址: http://www.djcxy.com/p/14275.html

    上一篇: How to manage object lifetime when working with COM interop?

    下一篇: How to control power to a USB port of a macbook?