我在 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,这对我们的一些客户来说是一个重大变化。如果有办法让我准确识别出这个错误,我们可以避免升级痛苦的时间更长一点。