如何在 Release() 上處 理 NET COM 互操作對象
我有一個用託管程式碼 (C++/CLI) 編寫的 COM 對象。我在標準 C++ 中使用該對象。
當 COM 對像被釋放時,如何強制我的 COM 對象的解構子立即被呼叫?如果這不可能,請呼叫我的 COM 對像上的 Release() 呼叫 MyDispose() 方法?
我聲明對象的程式碼(C++/CLI):
[Guid("57ED5388-blahblah")] [介面類型(ComInterfaceType::InterfaceIsIDispatch)] [ComVisible(真)] 公共介面類 IFoo { 無效必須(); }; [指南(“417E5293-blahblah”)] [ClassInterface(ClassInterfaceType::None)] [ComVisible(真)] 公共參考類 Foo : IFoo { 上市: 無效我的處置(); ~Foo() {MyDispose();} // 這永遠不會被呼叫 !Foo() {MyDispose();} // 這由垃圾收集器呼叫。 virtual ULONG Release() {MyDispose();} // 這永遠不會被呼叫 虛擬無效必須(); };我使用對象的程式碼(本機 C++):
#import "..\\Debug\\Foo.tlb" ... Bar::IFoo setup(__uuidof(Bar::Foo)); // 這個對象來自 .tlb。 setup.must(); 設置->釋放();// 顯式釋放,實際上沒有必要,因為 Bar::IFoo 的解構子將呼叫 Release()。如果我在我的 COM 對像上放置一個解構子方法,它就永遠不會被呼叫。如果我放置一個終結器方法,它會在垃圾收集器處理它時被呼叫。如果我明確呼叫我的 Release() 覆蓋,它永遠不會被呼叫。
我真的很喜歡這樣,當我的原生 Bar::IFoo 對象超出範圍時,它會自動呼叫我的 .NET 對象的處置程式碼。我想我可以通過重寫 Release() 來做到這一點,如果對象計數 = 0,則呼叫 MyDispose()。但顯然我沒有正確覆蓋 Release(),因為我的 Release() 方法從未被呼叫過。
顯然,我可以通過將 MyDispose() 方法放在介面中並要求使用我的對象的人在 Release() 之前呼叫 MyDispose() 來實現這一點,但是如果 Release() 只是清理了對象,那會更巧妙。
是否可以在釋放 COM 對象時強制立即呼叫 .NET COM 對象的解構子或其他方法?
Google搜尋這個問題讓我得到很多點擊,告訴我呼叫 System.Runtime.InteropServices.Marshal.ReleaseComObject(),但當然,這就是你告訴 .NET 釋放 COM 對象的方式。我希望 COM Release() 處理一個 .NET 對象。
實際上,當最後一個引用被釋放時,不會從 COM 客戶端呼叫 Dispose(或者我應該說 ~Foo)和 Release。它根本沒有實施。這裡有一些想法如何做到這一點。
http://blogs.msdn.com/oldnewthing/archive/2007/04/24/2252261.aspx#2269675
但是即使作者也不建議使用該方法。
如果您同時實現 COM 客戶端,最好的選擇是查詢 IDisposable 並顯式呼叫 Dispose,要請求的 iid 是:
{805D7A98-D4AF-3F0F-967F-E5CF45312D2C}我能想到的其他選擇是實現某種自己的“COM 垃圾收集器”。COM 創建的每個對像都將放在一個列表中(前提是您的類型的對像只能由 COM 創建 - 我想不出任何方法來區分對象的創建位置)。然後您必須定期檢查列表,並在每個對像上呼叫如下內容:
IntPtr iUnk = Marshal.GetIUnknownForObject(@object); int refCount = Marshal.Release(iUnk); if (refCount == 0) @object.Dispose();但這是一些瘋狂的想法。
我有一個用託管程式碼 (C++/CLI) 編寫的 COM 對象。我在標準 C++ 中使用該對象。當 COM 對像被釋放時,如何強制我的 COM 對象的解構子立即被呼叫?如果這不可能,我可以讓 Release() 在我的(託管 DotNet - GBG)COM 對像上呼叫 Dispose()(不是 MyDispose() - GBG)方法嗎?
RE:當客戶端是非託管程式碼時,強制釋放 DotNet COM 伺服器綁定的資源。 這些資源可以安排在垃圾收集器收集項目時釋放,但這不是確定性的,對於不經常進行垃圾收集的大型記憶體系統,文件流等資源可能在數小時或數天內都不會釋放。
這是 COM 可呼叫包裝器 (CCW) 的一個常見問題,可以從另一個有點相關的執行緒中看出: Is it possible to intercept (or be aware) COM Referencecounting on CLR objects 暴露給 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() 方法。我不確定微軟為什麼不這樣做,因為他們可以完全訪問程序集資訊,他們可以輕鬆確定 DotNet COM 類是否支持 IDisposable,如果支持,只需在最終版本上呼叫 Dispose(),因為這將在CCW,可以避免由於額外的介面引用而導致處理引用計數的所有復雜性。
我看不出這將如何“破壞”任何現有程式碼,因為任何可辨識 IDisposable 的客戶端仍然可以呼叫 Dispose(),如果根據上述模板實現,它只會在第一次呼叫時有效地執行任何操作。Microsoft 可能會擔心一個類被 Disposed,而仍然有對其的託管引用,直到嘗試使用已釋放的資源開始引發異常時才知道它被 Disposed,但這對於任何不正確的資源來說都是一個潛在的問題即使只有 DotNet 客戶端也使用 IDisposable 介面:如果對同一個對象實例有多個引用,並且其中任何一個呼叫 Dispose(),其他人會發現嘗試使用所需的已處置資源會導致異常。對於此類情況,
由於 Microsoft 沒有在 mscoree.dll 中實現 CCW 時完成所需的幾行程式碼,因此我編寫了一個圍繞 mscoree.dll 的包裝器,它添加了這個額外的功能。這有點複雜,為了控制圍繞任何 DotNet COM 類的任何實例創建包裝器,我還需要包裝 IClassFactory 介面並將 CCW 實例聚合到我的“CCW_Wrapper”包裝器類中。這個包裝器還支持來自另一個外部類的更多級別的聚合。該程式碼還對正在使用的 mscoree.dll 實現中的類的實例進行引用計數,以便在沒有引用時能夠在 mscoree.dll 上呼叫 FreeLibrary(如果以後需要,可以再次呼叫 LoadLibrary)。程式碼也應該是多執行緒友好的,這對於 Windows 7 下的 COM 是必需的。
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 文件夾中,然後使用您的安裝程序或您的
$$ COMRegisterFunction $$DotNet COM 伺服器中的方法將 InprocServer32 的類系統資料庫項從 mscoree.dll 修改為此包裝器的名稱(例如 mscoreeWrapper.dll)。可以在 32 位和/或 64 位下編譯,安裝在 64 位系統上需要將 64 位版本放入 System 文件夾,將 32 位版本放入 SysWOW64 文件夾;此外,正常 CLSID 註冊和虛擬化 WOW6432 版本都應針對 InprocServer32 條目進行修改。某些應用程序可能需要對這個包裝 DLL 進行數字簽名才能無縫工作,這完全是另一回事。如果有人願意,我會在此處提供這些 DLL 的編譯版本的連結。 正如我所說,所需的幾行(不包括包裝器要求)技術應該真正合併到 mscoree.dll 中。有誰知道如何联系 Microsoft 相關部門的人員以提出此建議?
EDITADD: 我已向 Microsoft Connect 送出了有關 DotNet 框架的建議。這似乎是向 Microsoft 提供回饋的最佳方式。
EDITADD2: 在實施解決此問題的方法時,我意識到為什麼 MIcrosoft 可能不會實施此“如果支持,當 CCW 引用計數降至零時自動呼叫 Dispose”。在編寫解決方法時,我必須獲得一個指向託管對像上的 COM 介面的引用指針,以便將其傳遞給純非託管 COM 方法,然後必須 Release() 引用計數,以便沒有強烈的 CCW引用該對象並因此導致記憶體洩漏,因為它永遠不會用於垃圾收集。我這樣做是因為我知道將託管對像上的引用計數減少到零目前只會使 CCW 放棄對對象的強引用,如果沒有其他引用,則使其有資格進行垃圾收集。但是,如果 Microsoft 按照我的建議實施了 Auto Dispose 修復程序,或者如果此程式碼已就位包裝 mscoree.dll 功能,則會在不需要時觸發託管對像上的 Dispose()。對於這種特殊情況,我可以“保護” Dispose(bool disposing) 虛擬方法以防止 Dispose() 發生,但是對於使用此行為的任何現有程式碼具有相同的假設,包括 Microsoft 的 DotNet 執行時庫的實現,實現CCW 上的這個“修復”會破壞現有的程式碼。這個包裝器修復仍然適用於自己編寫的 COM 伺服器,並且意識到這種副作用,因為他們可以在 Dispose() 上放置“防護”。
EDITADD 3: 在進一步的工作中,我看到我對 Microsoft 的建議仍然有效,並且可以通過在實現如果介面存在,則託管 COM 伺服器僅在新的自定義屬性(例如$$ AutoComDispose(true) $$,其預設值為 false,應用於託管 COM 伺服器類。通過這種方式,程序員將選擇實現該功能,並且有關新屬性的文件將警告其使用必須“保護” Dispose() 方法,例如在有可能發生的情況下使用“人工引用計數” Marshal.Release() 方法在託管伺服器使用的程式碼中顯式呼叫,或者通過呼叫 Marshal.GetObjectForIUnknown() 等方法隱式呼叫,如果 ComObject 是託管的,則在某些情況下可以呼叫參考點的 QueryInterface 和 Release目的。
如上所述,此答案的主要問題是安裝它以供使用的複雜性。