我遇到了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,结果如下:
我包含了我认为相关的代码,但如果需要更多代码,请大声说出来; 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,我们可以稍微避免升级的痛苦。
答案 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。
不幸的是,对于任何使用带有这种配置的应用程序的人来说,这会留下内存泄漏,但这暂时还不够。