CLR 2.0内存泄漏:卸载时无法释放COM对象

时间:2013-01-08 14:52:34

标签: memory-leaks clr com-interop managed-c++

我遇到了CLR 2.0中的一个在CLR 4.0中解决的错误。在跨.NET COM互操作传递数组并生成COM异常(E_FAIL)时会发生这种情况。有关如何重现此错误的详细信息如下。

我的问题是强制我们的客户升级到.NET 4.0是非常困难的,所以我想实现一个解决方法。如果我知道错误已经发生,我可以通过调用obj-> Release来这样做,但如果有任何误报的可能性,这显然很危险。

所以问题是:这个bug的规范是什么,我是否可以准确地识别它?

我发现了4.0.1,4.0.2和4.0.3的.NET发行说明,但未提及该错误。 CLR从2.0过渡到4.0时必须有一个重要的变更列表,我想这不是公开的吗?

显然,下面的代码本身没什么意义,但这是我可以根据相当大的复杂解决方案提炼的最简单的问题再现。

先谢谢你看看,

[R

重要编辑

不幸的是,我回过头来尝试进一步调查,下面的代码可能实际上没有重现错误,这将是令人失望的。但是,在实际应用中,内存泄漏很明显。如果有人感兴趣并且我有时间,我会尝试制作一个有效的例子。

代码概述

我有一个.NET应用程序,ConsoleApp.exe,这里用C#转载,虽然原文是F#。 ConsoleApp.exe调用托管程序集managed.AComObject.dll,它暴露COM对象AComObject。 AComObject.get_TheObject()返回指向智能指针ASmartPtr的VARIANT *,它允许我覆盖AddRef和Release方法以观察对象所持有的引用。

当运行启用了非托管代码调试的ConsoleApp.exe时,我可以在SmartPtr上看到引用计数。我通过调整ConsoleApp.exe.config中的supportedRuntime属性来更改CLR,结果如下:

  • v4.0显示“DEBUGMSG :: ASmartPtr :: Release:0”,此时SmartPtr将被删除。
  • v2.0.50727在退出之前显示“DEBUGMSG :: ASmartPtr :: Release:1”,泄漏。

我包含了我认为相关的代码,但如果需要更多代码,请大声说出来; COM需要很多样板代码......!

ConsoleApp.exe

using managed.AComObject;
using System;

public static class Program
{
    public static void Main()
    {
        AComObject an_obj = new AComObject();
        object[] pData = new object[] { 1 };
        object a_val = an_obj.get_TheObject(0, pData);
        object[] pData2 = new object[] { a_val };

        try
        {
            object obj3 = an_obj.get_TheObject(1, pData2);
        }
        catch (System.Exception)
        {
            // Makes no diff whether it's caught - still does not clean
        }
    }
}

AComObject.dll

AComObject.idl

interface IAComObject : IDispatch
{
    [propget, id(1), helpstring("")] HRESULT DllName([out, retval] BSTR* pName);
    [propget, id(2), helpstring("")] HRESULT TheObject([in] LONG count, [in, size_is(count)] VARIANT* pData, [out, retval] VARIANT* pObject);
};

[...]    
library AComObjectLib
{
    importlib("stdole2.tlb");

    // Class information
    [...]
    coclass AComObject
    {
        [default] interface IAComObject;
    };
};

AComObject.h

[...]

class ATL_NO_VTABLE CAComObject :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CAComObject, &CLSID_AComObject>,
    public IDispatchImpl<IAComObject, &IID_IAComObject, &LIBID_AComObjectLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
DECLARE_REGISTRY_RESOURCEID(IDR_ACOMOBJECT)

BEGIN_COM_MAP(CAComObject)
    COM_INTERFACE_ENTRY2(IDispatch, IAComObject)
    COM_INTERFACE_ENTRY(IAComObject)
END_COM_MAP()

public:
    CAComObject();

    virtual /* [helpstring][propget] */ HRESULT STDMETHODCALLTYPE get_DllName(
            /* [retval][out] */ BSTR* pName);

    virtual /* [helpstring][propget] */ HRESULT STDMETHODCALLTYPE get_TheObject(
            /* [in] */ LONG count,
            /* [in, size_is(count)] */ VARIANT* pData,
            /* [retval][out] */ VARIANT* pObject);
};

OBJECT_ENTRY_AUTO(CLSID_AComObject, CAComObject)

AComObject.cpp

class ASmartPtr : public IUnknown
{
    int m_RC;

    void DebugMsg(std::string msg)
    {
        std::stringstream _msg;
        _msg << ".\nDEBUGMSG::ASmartPtr::" << msg << "\n";
        OutputDebugStringA(_msg.str().c_str());
    }
public:
    ASmartPtr()
        : m_RC(1)
    {
        DebugMsg(std::string("Created"));
    }

    virtual ULONG STDMETHODCALLTYPE AddRef() 
    {
        ULONG refcnt = ++m_RC;
        std::stringstream msg;
        msg << "AddRef:" << refcnt;
        DebugMsg(msg.str());
        return refcnt;
    }

    virtual ULONG STDMETHODCALLTYPE Release() 
    {
        ULONG refcnt = --m_RC;
        std::stringstream msg;
        msg << "Release:" << refcnt;
        DebugMsg(msg.str());
        if (m_RC == 0)
            delete this;
        return refcnt;
    }

    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** ppvObj)
    {
        if (!ppvObj) return E_POINTER;

        if (iid == IID_IUnknown)
        {
            *ppvObj = this;
            AddRef();
            return NOERROR;
        }
        return E_NOINTERFACE;
    }
};

[...]

STDMETHODIMP CAComObject::get_TheObject(LONG count, VARIANT* pData, VARIANT* pObject)
{
    if (count == 1)
        return E_FAIL;

    CComVariant res;
    res.punkVal = new ASmartPtr();
    res.vt = VT_UNKNOWN;

    res.Detach(pObject);

    return S_OK;
}

managed.AComObject.dll

这是使用以下构建后事件从COM对象组装而来,以便将数组传递给get_TheObject()而不是引用。

批处理文件

call "C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\Tools\vsvars32.bat"

echo "f" | xcopy /L/D/Y ..\Debug\AComObject.dll  managed.AComObject.dll | find "AComObject" > nul
if not errorlevel 1 (
    tlbimp   ..\Debug\AComObject.dll /primary /keyfile:..\piakey.snk /out:managed.AComObject.dll
  ildasm managed.AComObject.dll /out:managed.AComObject.raw.il
  perl -p oneliner.pl < managed.AComObject.raw.il > managed.AComObject.il
  ilasm managed.AComObject.il /dll /key=..\piakey.snk
)
set errorlevel=0
exit 0

oneliner.pl

$a = 1 if (/TheObject\(/);if ($a){s/object&/object\[\]/; s/marshal\( struct\) pData/marshal\( \[\]\) pData/; $a++; $a&=3;}

这只是改变了IL:

[in] object&  marshal( struct) pData) runtime managed internalcall

[in] object[]  marshal( []) pData) runtime managed internalcall

一些其他信息

在考虑我对汉斯评论的回应时,我意识到缺少一些相关信息。

如果没有抛出异常(即E_FAIL变为S_OK),则没有泄漏。在S_OK的情况下,当我们将.NET COM互操作交回ConsoleApp.exe时,我们可以看到对象引用计数返回到1。在E_FAIL情况下,refcount保持为2.在这两种情况下,我们可以观察终结器在应用程序终止时再次减少引用计数(并观察S_OK情况下的对象析构函数),但在E_FAIL情况下,这仍然留下了refcount为1,因此对象泄露。在CLR 4.0中,所有行为都符合预期(即,即使在E_FAIL情况下,refcount在返回ConsoleApp.exe时返回到1)。

我们正在考虑升级到CLR 4.0以解决此漏洞,但它并非完全无关紧要,因为它以不同的方式处理COM包装的托管DLL,这对我们的一些客户来说是一个重大变化。如果有一种方法可以让我准确地识别出这个bug,我们可以稍微避免升级的痛苦。

1 个答案:

答案 0 :(得分:0)

最后,解决方案相当简单,我们无需升级即可继续。这是将supportedRuntime和附加属性添加到application.exe.config的旧技巧:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0" />
  </startup>
</configuration>

如果没有该属性,.NET2代码会并行加载到CLR2中,因此我们会遇到泄漏问题。该属性允许将.NET2代码直接加载到CLR4中,从而避免泄漏。这里有一个关于该属性的详细评论:http://www.marklio.com/marklio/PermaLink,guid,ecc34c3c-be44-4422-86b7-900900e451f9.aspx

不幸的是,对于任何使用带有这种配置的应用程序的人来说,这会留下内存泄漏,但这暂时还不够。