7

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

1 回答 1

3

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

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

ReleaseCOMObject这里有关于何时/何时不调用MSDN的全面描述。

话虽如此,您的 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 应用程序添加类似的诊断,并准确查看接收器接口上的引用计数何时更新(请注意,这假定两个项目的调试版本)。因此,例如,我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 运行调试客户端应用程序,您可以看到发生了什么。首先,在创建 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不会影响引用计数(其他人也注意到了),但还请注意,我们从removeC# COM 对象事件的访问器中获得了跟踪,这是强制进行一些垃圾收集或其他撕裂的可能位置- 下任务。

总结一下:

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

我真的希望这会有所帮助。在这篇旧的但非常出色的博客文章中有一些替代的 COM 处理技巧和更多解释。

于 2013-07-01T14:58:41.697 回答