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 を別の方法で処理するため、完全に簡単なことではありません。これは一部のクライアントにとって重大な変更です。このバグを正確に特定する方法があれば、アップグレードの苦痛をもう少し回避できるでしょう。