65.9K
CodeProject 正在变化。 阅读更多。
Home

如何在 .NET Framework 4.0 中调用 CoEEShutDownCom 并使用它来解决 .NET 和 Native COM 对象之间使用连接点时出现的挂起问题

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2016年4月23日

CPOL

3分钟阅读

viewsIcon

8398

强制 .NET Framework 在必要时释放 RCW 对象

引言

在本文中,我想讨论一种解决方案,用于解决 .NET 和 native COM 对象之间因连接点引起的挂起问题。

简而言之,挂起问题是由于垃圾回收不释放 native COM 接口 (RCW) 引起的,但 native COM 对象正在等待所有引用被释放。

Paresh Suthar 的博客提供了一种解决方案,可以在 .NET 代码中调用 Marshal.ReleaseComObject 来释放 RCW 以避免该问题,有关详细信息,请参见此链接

但是,如果我们无法更改 .NET 代码,则可以调用 CoEEShutDownCom 来释放所有 RCW 引用。 Adam Nathan 仅谈到了这种解决方案的可能性(.NET and COM: The Complete Interoperability Guide),但没有给出示例。 特别是,在 .NET 4.0 中,全局函数 CoEEShutDownCom 已被弃用。 我们需要做一些工作才能获得该解决方案。

背景 - RCW

RCW 定义可以在此处找到。

在我们的案例中,RCW 对象是 native sink 对象的包装器。

如何重现该问题

如果您有一个 MFC 应用程序(作为 COM 客户端)调用 C# 中实现的 COM 服务器,并且该 COM 服务器通过 C# 属性 ComSourceInterfaces 支持连接点,则在退出时可能会重现该问题。

Paresh Suthar 在解释如果您使用 ComSourceInterfaces 属性会发生什么方面做得很好,请参见介绍部分中的博客 URL。 当 native 应用程序是 MFC 时,该问题变得更加严重。 尝试退出时,应用程序可能会挂起。 (实际上,如果您创建一个低内存事件,这将导致进行垃圾回收操作,则应用程序将退出。但是,创建低内存事件不是一项稳定的工作。)

退出挂起是由 MFC 框架的 AfxOleCanExitApp() 检查引起的。 在 MFC 世界中,通常在 COM 对象的构造函数中调用 AfxOleLockApp,在析构函数中调用 AfxOleUnlockApp。 在我们的案例中,由于 sink 对象未被释放,因此未调用 AfxOleUnlockApp,sink 对象的最新引用在 RCW 中。

以下示例代码可以显示退出挂起问题,

class Sink : public EventInterfac
{
public:
    Sink()
    {
        AfxOleLockApp();
    }

    ~Sink()
    {
        AfxOleUnlockApp();
    }
public:
    HRESULT STDMETHODCALLTYPE QueryInterface(const IID &iid, void **ppvObj)
    {
        if (iid == __uuidof(IUnknown) || iid == __uuidof(IDispatch) || iid == __uuidof(EventInterfac))
        {
            *ppvObj = this;
            this->AddRef();
            return S_OK;
        }

        return E_NOINTERFACE;
    }

    ULONG STDMETHODCALLTYPE AddRef(void)
    {
        m_dwRef++;
        return m_dwRef;
    }
    ULONG STDMETHODCALLTYPE Release(void)
    {
        m_dwRef--;
        if (m_dwRef == 0)
        {
            delete this;
            return 0;
        }
        return m_dwRef;
    }
    HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *){ return E_NOTIMPL; }
    HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT, LCID, ITypeInfo **){ return E_NOTIMPL; }
    HRESULT STDMETHODCALLTYPE GetIDsOfNames
	(const IID &, LPOLESTR *, UINT, LCID, DISPID *){ return E_NOTIMPL; }
    HRESULT STDMETHODCALLTYPE Invoke
	(DISPID, const IID &, LCID, WORD, DISPPARAMS *, VARIANT *, EXCEPINFO *, UINT *)
    {
        return S_OK;
    }

public:
    DWORD m_dwRef = 1;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Sink* pSink = new Sink;

    CoInitialize(NULL);

    IManagedInterface* cpi;
    HRESULT hResult = CoCreateInstance(__uuidof(ComServer),
        NULL, CLSCTX_INPROC_SERVER,
        __uuidof(IManagedInterface), reinterpret_cast<void**>(&cpi));

    DWORD dwCookie = -1;

    IConnectionPointContainerPtr pCPContainer;
    hResult = cpi->QueryInterface(IID_IConnectionPointContainer,
        (void**)&pCPContainer);
    if (SUCCEEDED(hResult))
    {
        IConnectionPointPtr pEventCP = NULL;
        hResult = pCPContainer->FindConnectionPoint(__uuidof(EventInterfac),
            &pEventCP);
        if (SUCCEEDED(hResult))
        {
            pEventCP->Advise(pSink, &dwCookie);
        }

        pEventCP->Release();
        pEventCP = NULL;
    }
   
    for (int i = 0; i < 100; i++)
    {
        CComBSTR bstrTest(L"Test");
        cpi->PrintHi((BSTR)bstrTest);
    }

    IConnectionPointPtr pEventCP = NULL;
    hResult = pCPContainer->FindConnectionPoint(__uuidof(EventInterfac),
        &pEventCP);
    if (SUCCEEDED(hResult))
    {
        pEventCP->Unadvise(dwCookie);
    }

    pEventCP->Release();
    pEventCP = NULL;

    pSink->Release();

    while (!AfxOleCanExitApp())
    {
        Sleep(1000);
    }

    CoUninitialize();

    return 0;
}

C# COM 服务器对象源代码如下

namespace CSharpComwithCP
{
    [ComVisible(true)]
    [Guid("901EE2A0-C47C-43ec-B433-985C02004321")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    // The public interface describing the events of the control
    public interface EventInterfac
    {
        [DispId(1)]
        void Hello();
    
    }

    [ComVisible(true)]
    [Guid("DBE0E8C4-1C61-41f3-B6A4-4E2F35354321")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IManagedInterface
    {
        int PrintHi(string name);
    }

    [ComVisible(true),
    ComSourceInterfaces(typeof(EventInterfac)),
    ClassInterface(ClassInterfaceType.None),
    Guid("80B59B58-98EA-303C-BE83-D26E5D8D4321")]
    public class ComServer : IManagedInterface
    {
        public void FireHelloEvent()
        {
            if (null != _Hello)
                _Hello();
        }

        public int PrintHi(string name)
        {
            Func1();
            return name.Length;
        }

        [ComVisible(false)]
        public delegate void HelloEventHandler();
        public event HelloEventHandler Hello
        {
            add { _Hello += value; }
            remove
            {
                _Hello -= value;
            }
        }

        private event HelloEventHandler _Hello = null;
    }
}

这些只是示例,在现实世界中,如果您正在使用 MFC 实现 ActiveX 容器,并使用 C# 实现 ActiveX,您将遇到同样的问题。

解决方案

在我的实际案例中,我无法接受 Paresh Suthar 的建议来更改 .NET 代码(它们是我客户的源代码),我只能更改 native sink 对象。 在 Google 上进行深入搜索并阅读代码后,我相信 Adam Nathan 的建议,使用 CoEEShutDownCom 释放所有 RCW 的引用应该是解决方案。

“调用 CoEEShutDownCOM 强制 CLR 释放它在 RCW 内部保存的所有接口指针。 通常不需要调用此方法,但如果您正在运行泄漏检测代码或以某种方式依赖于在进程终止之前释放所有接口指针,则可能是必要的。”
--
.NET and COM - The Complete Interoperability Guide by Adam Nathan。

然后,我查看了 MSDN,全局 CoEEShutDownCOM 已被弃用。 我们需要使用以下代码在 native 世界中实现全局函数。

void CoEEShutdownCOM()
{
    ULONG aFetched = 1;
    ICLRMetaHost *meta = NULL;
    ICLRRuntimeInfo *info = NULL;

    HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (void **)&meta);

    IEnumUnknown * pRtEnum = NULL;

    DWORD Aid = 0;
    ULONGLONG bytes = 0;
    ULONG fetched = 0;

    hr = meta->EnumerateLoadedRuntimes(GetCurrentProcess(), &pRtEnum);

    while ((hr = pRtEnum->Next(1, (IUnknown **)&info, &fetched)) == S_OK && fetched > 0)
    {
        void (*pfunc)();
        info->GetProcAddress("CoEEShutDownCOM", (void**)&pfunc);
        if (pfunc)
        {
            pfunc();
        }

        info->Release();
    }

    pRtEnum->Release();
    meta->Release();
}

AfxOleCanExitApp() 检查之前调用此函数,世界就得救了。

讨论

即使该问题可以通过此函数解决,但它并不完美。 退出时,我们正在强制释放所有 RCW 对象引用。 也许,某些引用仍然需要有人使用。 在应用此解决方案之前,我们需要考虑这种可能性。

我想知道,如果我们有一种方法(在 native 世界中)来释放一个特定的 RCW 对象,我们知道它是我们 sink 的包装器,这将是一个完美的解决方案。 但是我没有找到任何关于此的解决方案,即使它是否可行。 非常欢迎您对此方法提供评论。 或任何其他建议?

结论

在本文中,我给出了示例代码来重现由 .NET 和 native sink 对象之间的连接点技术引起的挂起问题。 并提供了解决方案代码。

.NET 解决方案可以在 Paresh Suthar 的博客中找到。

Native 解决方案最初由 Adam Nathan 提及,我仅提供 .NET Framework 4.0 中的示例代码。

历史

  • 2016 年 4 月 23 日 - 第一个版本
© . All rights reserved.