我有一个用托管代码 (C++/CLI) 编写的 COM 对象。我在标准 C++ 中使用该对象。
当 COM 对象被释放时,如何强制立即调用 COM 对象的析构函数?如果不可能,我可以让 Release() 在我的(托管 DotNet - GBG)COM 对象上调用 Dispose() (不是 MyDispose() - GBG)方法吗?
RE: 当客户端是非托管代码时,强制确定性地释放 DotNet COM Server 绑定的资源。这些资源可以安排在垃圾收集器收集项目时释放,但这不是确定性的,对于垃圾收集不频繁的大型内存系统,文件流等资源可能数小时或数天都不会释放。
这是 COM Callable Wrappers (CCW) 的一个常见问题,可以通过另一个相关的线程看到:是否可以拦截(或了解)对暴露给 COM 的 CLR 对象进行 COM 引用计数。在这种情况下,就像在编写自己的 COM 客户端的任何情况下一样,无论是在托管代码还是非托管代码下,只需调用 IDisposable.Dispose() 方法即可轻松解决,就像在那里所做的那样。但是,该方法不适用于(例如)DotNet COM 编解码器类,该类的客户端可能是操作系统本身,并且客户端不必知道 COM 服务器是非托管或托管 (DotNet)。
可以按照 MSDN 链接在 DotNet COM 服务器上实现 IDisposable.Dispose() 模式:http://msdn.microsoft.com/en-us/library/system.idisposable.aspx,但这不会有任何好处,因为 Dispose() 方法永远不会被 CCW 调用。理想情况下,如果作为 CCW 发布和/或终结/析构函数的一部分实现,则 mscoree.dll 中 CCW 的实现应该真正检查并调用 IDisposable.Dispose() 方法。我不确定为什么 Microsoft 不这样做,因为可以完全访问程序集信息,他们可以轻松确定 DotNet COM 类是否支持 IDisposable,如果支持,只需在最终版本上调用 Dispose() 即可,因为这将在CCW 中,由于额外的接口引用而导致的处理引用计数的所有复杂性都可以避免。
我看不出这会如何“破坏”任何现有代码,因为任何识别 IDisposable 的客户端仍然可以调用 Dispose(),如果根据上面的模板实现,它只会在第一次调用时有效地执行任何操作。微软可能会担心一个类被处置,而仍然存在对它的托管引用,直到尝试使用已处置的资源开始引发异常时,这些引用才知道它被处置,但这对于任何不当行为都是一个潜在的问题即使仅使用 DotNet 客户端,也可使用 IDisposable 接口:如果对同一对象实例有多个引用,并且其中任何一个调用 Dispose(),则其他引用会发现尝试使用所需的已处置资源会导致异常。对于这种情况,应该始终使用处置布尔值(根据 IDisposable 模式模板)放置防护,或者仅通过公共包装器引用对象。
由于 Microsoft 尚未完成在 mscoree.dll 中实现 CCW 所需的几行代码,因此我在 mscoree.dll 周围编写了一个包装器来添加此额外功能。它有点复杂,因为为了控制围绕任何 DotNet COM 类的任何实例创建包装器,我还需要包装 IClassFactory 接口并将 CCW 实例聚合在我的“CCW_Wrapper”包装器类中。该包装器还支持来自另一个外部类的更高级别的聚合。该代码还对正在使用的 mscoree.dll 实现中的类实例进行引用计数,以便能够在没有引用时调用 mscoree.dll 上的 FreeLibrary(如果需要,稍后可以再次调用 LoadLibrary)。该代码还应该是多线程友好的,正如 Windows 7 下 COM 所需要的那样。我的 C++ 代码如下:
2010 年 12 月 22 日编辑:消除了 COM_Wrapper 构造函数的一个不必要的参数:
#include <windows.h>
HMODULE g_WrappedDLLInstance = NULL;
ULONG g_ObjectInstanceRefCnt = 0;
//the following is the C++ definition of the IDisposable interface
//using the GUID as per the managed definition, which never changes across
//DotNet versions as it represents a hash of the definition and its
//namespace, none of which can change by definition.
MIDL_INTERFACE("805D7A98-D4AF-3F0F-967F-E5CF45312D2C")
IDisposable : public IDispatch {
public:
virtual VOID STDMETHODCALLTYPE Dispose() = 0;
};
class CCW_Wrapper : public IUnknown {
public:
// constructor and destructor
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(); //to clear the reference count caused by the above.
}
this->ready = TRUE; //enable destruction of the object when release decrements to zero.
//OUTER IUNKNOWN OBJECTS MUST ALSO PROTECT THEIR DESTRUCTORS IN SIMILAR MANNERS!!!!!
}
~CCW_Wrapper() {
this->ready = FALSE; //protect from re-entering this destructor when object released to zero.
if (this->iWrappedIDisposable) {
//the whole reason for this project!!!!!!!!
this->iWrappedIDisposable->Dispose();
//the following may be redundant, but to be sure...
this->iOuterIUnknown->AddRef();
this->iWrappedIDisposable->Release();
}
if (this->iWrappedIUnknown)
this->iWrappedIUnknown->Release();
if (!InterlockedDecrement(&g_ObjectInstanceRefCnt)) {
//clear all global resources including the mutex, multithreading safe...
HMODULE m = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)0);
if (m)
FreeLibrary(m);
}
}
// IUnknown Interface
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) //if not being constructed or destructed...
delete this;
return 0;
}
private:
IUnknown *iOuterIUnknown;
IUnknown *iWrappedIUnknown;
IDisposable *iWrappedIDisposable;
BOOL ready;
ULONG refcnt;
};
class ClassFactoryWrapper : public IClassFactory {
public:
// constructor and destructor
ClassFactoryWrapper(IClassFactory *icf) : wrappedFactory(icf), refcnt(0), lockcnt(0) {
InterlockedIncrement(&g_ObjectInstanceRefCnt);
}
~ClassFactoryWrapper() {
if (wrappedFactory)
wrappedFactory->Release();
if (!InterlockedDecrement(&g_ObjectInstanceRefCnt)) {
//clear all global resources, multithreading safe...
HMODULE m = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)0);
if (m)
FreeLibrary(m);
}
}
// IUnknown Interface
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;
}
// IClassFactory Interface
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;
}
//record the HMODULE instance in global variable for freeing later, multithreaded safe...
hDLL = (HMODULE)InterlockedExchangePointer((PVOID*)&g_WrappedDLLInstance, (LPVOID)hDLL);
if (hDLL)
FreeLibrary(hDLL);
if (FAILED(result = cfw->QueryInterface(IID_IClassFactory, ppv)))
delete cfw; //will automatically free library and the held class factory reference if necessary.
}
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;
}
DLL 还需要一个“.def”文件,如下所示:
LIBRARY mscoreeCOM_DisposeWrapper
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
要使用此源代码,请将其编译为 DLL 并将 DLL 安装到 Windows SYSTEM 文件夹中,然后让安装程序或 DotNet COM 服务器中的 [COMRegisterFunction] 方法将 InprocServer32 的类注册表项从 mscoree.dll 修改为该包装器的名称(例如 mscoreeWrapper.dll)。它可以在32位和/或64位下编译,如果要在64位系统上安装,请将64位版本放入System文件夹中,将32位版本放入SysWOW64文件夹中;此外,正常的 CLSID 注册和虚拟化 WOW6432 版本都应针对 InprocServer32 条目进行修改。某些应用程序可能需要对该包装 DLL 进行数字签名才能无缝工作,这完全是另一个主题。如果有人需要,我将在此处提供这些 DLL 的编译版本的链接。
正如我所说,所需的几行(不包括包装器要求)技术确实应该合并到 mscoree.dll 中。有谁知道如何联系 Microsoft 相应部门的人员提出此建议?
EDITADD:我已经提交了一份建议用于将 DotNet 框架连接到 Microsoft Connect。这似乎是向微软提供反馈的最佳方式。
编辑DD2:在实现此问题的解决方法时,我意识到为什么 MIcrosoft 可能不会实现“当 CCW 引用计数降至零时,如果支持的话,自动调用 Dispose”。在编写解决方法时,我必须获取指向托管对象上的 COM 接口的引用指针,以便将其传递给纯非托管 COM 方法,然后必须 Release() 该引用计数,以便不强烈使用 CCW引用该对象,因此由于永远无法将其用于垃圾回收而导致内存泄漏。我这样做是因为我知道当前,将托管对象上的引用计数减少到零只会使 CCW 删除对该对象的强引用,如果没有其他引用,则使其有资格进行垃圾回收。但是,如果 Microsoft 按照我的建议实现了 Auto Dispose 修复,或者如果此代码已就位包装了 mscoree.dll 功能,则这会在不需要时在托管对象上触发 Dispose()。对于这种特殊情况,我可以“保护” Dispose(bool dispose) 虚拟方法以防止 Dispose() 发生,但对于在相同假设下使用此行为的任何现有代码,包括 Microsoft 的 DotNet 运行时库的实现,在 CCW 上实现此“修复”将破坏现有代码。此包装器修复仍然适用于自己编写的 COM 服务器,并意识到这种副作用,因为它们可以在 Dispose() 上放置“防护”。
编辑3:在进一步的工作中,我发现我向 Microsoft 提出的建议仍然有效,并且可以通过在实现托管 COM 服务器的对象实例上调用 IDisposable.Dispose() 方法的修复来避免“破坏”现有代码的问题。如果接口存在仅当新的自定义属性(例如 [AutoComDispose(true)])(其默认值为 false)应用于托管 COM 服务器类时。通过这种方式,程序员将选择实现该功能,并且有关新属性的文档将警告其使用时必须“保护”Dispose() 方法,例如在可能出现以下情况时使用“人工引用计数” Marshal.Release() 方法由托管服务器使用的代码显式调用,或者通过调用 Marshal.GetObjectForIUnknown() 等方法隐式调用,在某些情况下,如果 ComObject 是托管的,则可以调用参考点的 QueryInterface 和 Release目的。
该答案的主要问题是安装它以供使用的复杂性,如上所述。