我有一个用托管代码(C++/CLI)编写的COM对象。我在标准C++中使用该对象。
如何在释放COM对象时立即调用其析构函数?如果不可能,是否可以在我的(托管DotNet - GBG)COM对象上调用Dispose()方法,而不是MyDispose()方法(GBG)?
关于DotNet COM服务器绑定资源的确定性释放的问题。这些资源可以在垃圾收集器收集项目时释放,但这并不是确定性的,对于垃圾收集不频繁的大型内存系统,例如文件流等资源可能需要等待几个小时或几天才能被释放。
这是COM Callable Wrappers (CCW's) 的常见问题,如另一篇相关的文章所示:
Is it possible to intercept (or be aware of) COM Reference counting on CLR objects exposed to COM. 在那种情况下,就像在任何自己编写COM客户端的情况下一样,无论是在托管代码还是非托管代码下,只需调用IDisposable.Dispose()方法即可解决问题。然而,对于一个DotNet COM编解码器类的客户端可以是操作系统本身,客户端不应该知道COM服务器是托管还是非托管的。
一个DotNet COM服务器可以按照MSDN链接中的IDisposable.Dispose()模式进行实现:
http://msdn.microsoft.com/en-us/library/system.idisposable.aspx,但这没用,因为CCW永远不会调用Dispose()方法。理想情况下,mscoree.dll中的CCW实现应该检查并调用IDisposable.Dispose()方法,如果它作为CCW释放和/或终结器的一部分实现。我不确定微软为什么不这样做,因为他们完全可以通过程序集信息轻松确定DotNet COM类是否支持IDisposable,并在最终释放时调用Dispose(),而且由于这将在CCW内部完成,所有有关处理额外接口引用所需的引用计数的复杂性都可以避免。
我看不出这会"破坏"任何现有代码,因为任何具有IDisposable意识的客户端都可以调用Dispose(),如果按照以上模板实现,它只在第一次调用时有效。微软可能担心的是在仍然存在管理引用的情况下将类Dispose掉,直到开始使用已经Dispose的资源才会抛出异常,但即使只有DotNet客户端使用IDisposable接口,任何不当使用IDisposable接口的情况都有潜在问题:如果同一对象实例有多个引用,其中任何一个调用Dispose(),其他人将发现尝试使用所需的Disposed资源会导致异常。对于这种情况,应始终使用disposing布尔值(如IDisposable模式模板所示)或仅通过公共包装器引用对象。
自从微软没有在mscoree.dll的CCW实现中完成所需的几行代码,我编写了一个包装器来添加这个额外的功能。它有一点复杂,因为为了控制我包装程序的创建过程,我需要同时包装IClassFactory接口并将CCW实例聚合在我的"CCW_Wrapper"包装类中。这个包装器还支持从另一个外部类进一步的聚合级别。代码还对正在使用的mscoree.dll实现中的类实例进行引用计数,以便在没有引用时调用FreeLibrary(mscoree.dll),并在稍后需要时再次调用LoadLibrary。代码应该是多线程友好的,因为这是Windows 7下COM所必需的。我的C++代码如下:
#include <windows.h>
HMODULE g_WrappedDLLInstance = NULL;
ULONG g_ObjectInstanceRefCnt = 0;
MIDL_INTERFACE("805D7A98-D4AF-3F0F-967F-E5CF45312D2C")
IDisposable : public IDispatch {
public:
virtual VOID STDMETHODCALLTYPE Dispose() = 0;
};
class CCW_Wrapper : public IUnknown {
public:
CCW_Wrapper(
__in IClassFactory *pClassFactory,
__in IUnknown *pUnkOuter) :
iWrappedIUnknown(nullptr),
iOuterIUnknown(pUnkOuter),
iWrappedIDisposable(nullptr),
ready(FALSE),
refcnt(0) {
InterlockedIncrement(&g_ObjectInstanceRefCnt);
if (!this->iOuterIUnknown)
this->iOuterIUnknown = static_cast<IUnknown*>(this);
pClassFactory->CreateInstance(
this->iOuterIUnknown,
IID_IUnknown,
(LPVOID*)&this->iWrappedIUnknown);
if (this->iWrappedIUnknown) {
if (SUCCEEDED(this->iWrappedIUnknown->QueryInterface(__uuidof(IDisposable), (LPVOID*)&this->iWrappedIDisposable)))
this->iOuterIUnknown->Release();
}
this->ready = TRUE;
}
~CCW_Wrapper() {
this->ready = FALSE;
if (this->iWrappedIDisposable) {
this->iWrappedIDisposable->Dispose();
this->iOuterIUnknown->AddRef();
this->iWrappedIDisposable->Release();
}
if (this->iWrappedIUnknown)
this->iWrappedIUnknown->Release();
if (!InterlockedDecrement(&g_ObjectInstanceRefCnt)) {
HMODULE m = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)0);
if (m)
FreeLibrary(m);
}
}
STDMETHOD(QueryInterface)(REFIID riid, void **ppv) {
if (ppv) {
*ppv = nullptr;
if (riid == IID_IUnknown) {
*ppv = static_cast<IUnknown*>(this);
this->AddRef();
return S_OK;
}
else if (this->iWrappedIUnknown) {
return this->iWrappedIUnknown->QueryInterface(riid, ppv);
}
return E_NOINTERFACE;
}
return E_INVALIDARG;
}
STDMETHOD_(ULONG, AddRef)() {
return InterlockedIncrement(&this->refcnt);
}
STDMETHOD_(ULONG, Release)() {
if (InterlockedDecrement(&this->refcnt))
return this->refcnt;
if (this->ready)
delete this;
return 0;
}
private:
IUnknown *iOuterIUnknown;
IUnknown *iWrappedIUnknown;
IDisposable *iWrappedIDisposable;
BOOL ready;
ULONG refcnt;
};
class ClassFactoryWrapper : public IClassFactory {
public:
ClassFactoryWrapper(IClassFactory *icf) : wrappedFactory(icf), refcnt(0), lockcnt(0) {
InterlockedIncrement(&g_ObjectInstanceRefCnt);
}
~ClassFactoryWrapper() {
if (wrappedFactory)
wrappedFactory->Release();
if (!InterlockedDecrement(&g_ObjectInstanceRefCnt)) {
HMODULE m = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)0);
if (m)
FreeLibrary(m);
}
}
STDMETHOD(QueryInterface)(REFIID riid, void **ppv) {
if (ppv) {
*ppv = nullptr;
if (riid == IID_IUnknown) {
*ppv = static_cast<IUnknown*>(this);
this->AddRef();
}
else if (riid == IID_IClassFactory) {
*ppv = static_cast<IClassFactory*>(this);
this->AddRef();
}
else {
return E_NOINTERFACE;
}
return S_OK;
}
return E_INVALIDARG;
}
STDMETHOD_(ULONG, AddRef)() {
return InterlockedIncrement(&this->refcnt);
}
STDMETHOD_(ULONG, Release)() {
if (InterlockedDecrement(&this->refcnt) || this->lockcnt)
return this->refcnt;
delete this;
return 0;
}
STDMETHOD(CreateInstance)(IUnknown *pUnkOuter, REFIID riid, void **ppv) {
HRESULT result = E_INVALIDARG;
if (ppv) {
*ppv = nullptr;
if (pUnkOuter && (riid != IID_IUnknown))
return result;
CCW_Wrapper *oipm = new CCW_Wrapper(wrappedFactory, pUnkOuter);
if (!oipm)
return E_OUTOFMEMORY;
if (FAILED(result = oipm->QueryInterface(riid, ppv)))
delete oipm;
}
return result;
}
STDMETHOD(LockServer)(BOOL fLock) {
if (fLock)
InterlockedIncrement(&this->lockcnt);
else {
if (!InterlockedDecrement(&this->lockcnt) && !this->refcnt)
delete this;
}
return wrappedFactory->LockServer(fLock);
}
private:
IClassFactory *wrappedFactory;
ULONG refcnt;
ULONG lockcnt;
};
STDAPI DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, __deref_out LPVOID FAR* ppv) {
HRESULT result = E_INVALIDARG;
if (ppv) {
*ppv = nullptr;
if ((riid != IID_IUnknown) && (riid != IID_IClassFactory))
return E_NOINTERFACE;
HMODULE hDLL = LoadLibrary(L"mscoree.dll");
if (!hDLL)
return E_UNEXPECTED;
typedef HRESULT (__stdcall *pDllGetClassObject) (__in REFCLSID, __in REFIID, __out LPVOID *);
pDllGetClassObject DllGetClassObject = (pDllGetClassObject)GetProcAddress(hDLL, "DllGetClassObject");
if (!DllGetClassObject) {
FreeLibrary(hDLL);
return E_UNEXPECTED;
}
IClassFactory *icf = nullptr;
if (FAILED(result = (DllGetClassObject)(rclsid, IID_IClassFactory, (LPVOID*)&icf))) {
FreeLibrary(hDLL);
return result;
}
ClassFactoryWrapper *cfw = new ClassFactoryWrapper(icf);
if (!cfw) {
icf->Release();
FreeLibrary(hDLL);
return E_OUTOFMEMORY;
}
hDLL = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)hDLL);
if (hDLL)
FreeLibrary(hDLL);
if (FAILED(result = cfw->QueryInterface(IID_IClassFactory, ppv)))
delete cfw;
}
return result;
}
extern "C"
HRESULT __stdcall DllCanUnloadNow(void) {
if (g_ObjectInstanceRefCnt)
return S_FALSE;
return S_OK;
}
extern "C"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved ) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
一个'.def'文件也是DLL所必需的,如下所示:
LIBRARY mscoreeCOM_DisposeWrapper
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
要使用这个源代码,需要将其编译成DLL并安装到Windows系统文件夹中,然后让您的安装程序或DotNet COM服务器中的[COMRegisterFunction]方法修改InprocServer32的类注册表项,从mscoree.dll改为此包装器的名称(比如mscoreeWrapper.dll)。它可以在32位和/或64位下编译,并且在64位系统上安装时应该将64位版本放入System文件夹,将32位版本放入SysWOW64文件夹;此外,普通的CLSID注册和虚拟化的WOW6432版本都应该修改InprocServer32条目。有些应用程序可能需要此包装器DLL进行数字签名,以实现无缝工作,这是一个完全不同的主题。如果有人需要,我会在这里提供我的已编译版本的这些DLL的链接。
正如我所说,所需的几行代码(不包括包装器要求)技术应该真正地纳入mscoree.dll中。有人知道如何联系Microsoft内部适当部门的人来提出这个建议吗?
编辑添加:我已经向Microsoft Connect提交了一个建议,希望对DotNet Framework进行反馈。这似乎是向Microsoft提供反馈的最佳方式。
编辑添加2: 在解决这个问题时,我意识到为什么微软不会实现“当CCW引用计数降至零时自动调用Dispose(如果支持)”。在编写解决方法时,我必须获取托管对象上的COM接口的引用指针,以便将其传递给纯非托管COM方法,然后必须释放该引用计数,以便CCW不强制引用对象,从而导致内存泄漏,并且永远不能进行垃圾回收。我之所以这样做是因为我知道“当前仅通过减少托管对象的引用计数使CCW放弃对该对象的强引用”,如果没有其他引用,则可以将其标记为可回收。但是,如果微软按照我的建议实施了Auto Dispose修复或者如果此代码包装了mscoree.dll功能,则会在不需要时触发对托管对象的Dispose()。对于这种情况,我可以“保护”Dispose(bool disposing)虚拟方法以防止发生Dispose(),但是对于使用相同假设的任何现有代码,包括Microsoft的DotNet运行时库实现,将此“修复”应用于CCW会破坏该现有代码。这个包装器修复仍然适用于自己编写并且知道这种副作用的COM服务器,因为它们可以在Dispose()上放置“保护”。
EDITADD 3: 在进一步的工作中,我发现我的建议对于微软仍然是有效的,可以通过修复调用实现托管COM服务器的对象实例上IDisposable.Dispose()方法来避免“破坏”现有代码的问题,仅当新的自定义属性(例如[AutoComDispose(true)])应用于托管COM服务器类时,默认值为false。通过这种方式,程序员将选择实现功能,并且有关新属性的文档将警告其使用,因为必须“保护”Dispose()方法,例如使用“人工引用计数”,当存在由托管服务器使用的代码显式调用Marshal.Release()方法或者隐式调用像Marshal.GetObjectForIUnknown()这样的方法时,如果ComObject是托管对象,则在某些情况下可能会调用引用点的QueryInterface和Release。
这个答案的主要问题是安装它以供使用的复杂性,如上所述。