0

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

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

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

我找到了 4.0.1、4.0.2 和 4.0.3 的 .NET 发行说明,但没有提到该错误。从 2.0 到 4.0 的 CLR 转换中一定有一个重要的变更列表,我猜这不是公开的?

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

提前感谢您的观看,

R

重要编辑

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

代码概述

我有一个 .NET 应用程序 ConsoleApp.exe,虽然原来是 F#,但这里用 C# 复制。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 需要大量样板代码...!

控制台应用程序

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 情况下,我们可以看到对象引用计数返回 1,因为我们将 .NET COM 互操作返回到 ConsoleApp.exe。在 E_FAIL 情况下,引用计数保持在 2。在这两种情况下,我们都可以观察到终结器在应用程序终止时再次减少引用计数(并在 S_OK 情况下观察对象析构函数),但在 E_FAIL 情况下,这仍然会留下refcount 为 1,因此对象被泄漏。在 CLR 4.0 中,所有行为都符合预期(即,即使在 E_FAIL 情况下,refcount 在传递回 ConsoleApp.exe 时也会返回 1)。

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

4

1 回答 1

0

最后,解决方案相当简单,我们无需升级就可以继续进行。这是向 application.exe.config 添加supportedRuntime 和附加属性的老技巧:

<?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

不幸的是,对于任何使用具有这种配置的应用程序的人来说,内存泄漏仍然存在,但这暂时就足够了。

于 2013-02-07T14:32:02.340 回答