0

CLR 4.0 で解決された CLR 2.0 のバグに遭遇しました。これは、.NET COM 相互運用機能を介して配列を渡すときに発生し、COM 例外が生成されます (E_FAIL)。このバグの再現方法の詳細は以下のとおりです。

私の問題は、クライアントに .NET 4.0 へのアップグレードを強制するのが非常に難しいことです。そのため、回避策を実装したいと考えています。バグが発生したことがわかっている場合は obj->Release を呼び出してそうすることができますが、誤検知の可能性がある場合、これは明らかに危険です。

質問: このバグの仕様は何ですか? また、それを正確に特定することは可能ですか?

4.0.1、4.0.2、および 4.0.3 の .NET リリース ノートを見つけましたが、バグについては言及されていません。CLR 2.0 から 4.0 への移行には重要な変更リストがあるはずですが、これは公開されていないと思いますか?

明らかに、以下のコードはそれ自体ではほとんど意味がありませんが、非常に大規模で複雑なソリューションに基づいて抽出できる問題の最も単純な再現です。

ご覧いただきありがとうございます。

R

重要な編集

残念ながら、私は戻ってもう少し調査を試みましたが、以下のコードが実際にはバグを再現していない可能性があり、残念です. ただし、実際のアプリケーションでは、メモリ リークは明らかです。誰かが興味を持っていて、時間があれば、有効な例を作成しようとします.

コードの概要

元は F# ですが、ここでは C# で再現された .NET アプリケーション ConsoleApp.exe があります。ConsoleApp.exe は、COM オブジェクト AComObject を公開するマネージ アセンブリ managed.AComObject.dll を呼び出します。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

これは、参照ではなく get_TheObject() に配列を渡すことができるように、次のビルド後のイベントを使用して COM オブジェクトからアセンブルされます。

バッチファイル

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

いくつかの追加情報

Hans のコメントに対する私の回答を検討する中で、いくつかの関連情報が欠落していることに気付きました。

例外がスローされない場合 (つまり、E_FAIL が S_OK に変更された場合)、リークはありません。S_OK の場合、.NET COM 相互運用機能を介して ConsoleApp.exe に戻ると、オブジェクト参照カウントが 1 に戻ることがわかります。E_FAIL の場合、refcount は 2 のままです。どちらの場合も、アプリケーションが終了すると、ファイナライザーが再び refcount を減らすのを観察できます (S_OK の場合はオブジェクト デストラクタを観察します)。 refcount が 1 であるため、オブジェクトがリークされます。CLR 4.0 では、すべてが期待どおりに動作します (つまり、refcount は、E_FAIL の場合でも ConsoleApp.exe に戻ると 1 に戻ります)。

このリークを解決するために CLR 4.0 にアップグレードすることを検討していますが、COM でラップされたマネージ DLL を別の方法で処理するため、完全に簡単なことではありません。これは一部のクライアントにとって重大な変更です。このバグを正確に特定する方法があれば、アップグレードの苦痛をもう少し回避できるでしょう。

4

1 に答える 1

0

In the end, the solution was rather simple, and we were able to proceed without upgrading. It was the old trick of adding a supportedRuntime and the additional attribute to the application.exe.config:

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

Without the attribute, the .NET2 code loads side-by-side into CLR2, and so we suffer the leak. The attribute allows the .NET2 code to be loaded directly into the CLR4, hence avoiding the leak. There is a detailed review of that attribute here: http://www.marklio.com/marklio/PermaLink,guid,ecc34c3c-be44-4422-86b7-900900e451f9.aspx.

This unfortunately leaves the memory leak extant for anyone using an application with such a config, but this is adequate for the time being.

于 2013-02-07T14:32:02.340 に答える