簡體   English   中英

如何在Release()上處理NET COM互操作對象

[英]How to dispose of a NET COM interop object on Release()

我有一個用托管代碼(C ++ / CLI)編寫的COM對象。 我在標准C ++中使用該對象。
當COM對象被釋放時,如何強制立即調用COM對象的析構函數? 如果那是不可能的,請調用我有Release()調用我的COM對象上的MyDispose()方法?

我的代碼聲明對象(C ++ / CLI):

[Guid("57ED5388-blahblah")]
    [InterfaceType(ComInterfaceType::InterfaceIsIDispatch)]
    [ComVisible(true)]
    public interface class IFoo
    {
        void Doit();
    };

    [Guid("417E5293-blahblah")]
    [ClassInterface(ClassInterfaceType::None)]
    [ComVisible(true)]
    public ref class Foo : IFoo
    {
    public:
        void MyDispose();
        ~Foo() {MyDispose();} // This is never called
        !Foo() {MyDispose();} // This is called by the garbage collector.
        virtual ULONG Release() {MyDispose();} // This is never called
        virtual void Doit();
    };

我使用該對象的代碼(本機C ++):

#import "..\\Debug\\Foo.tlb"
...
Bar::IFoo setup(__uuidof(Bar::Foo)); // This object comes from the .tlb.
setup.Doit();
setup->Release(); // explicit release, not really necessary since Bar::IFoo's destructor will call Release().

如果我在我的COM對象上放置析構函數方法,則永遠不會調用它。 如果我放置一個終結器方法,它會在垃圾收集器到達時調用它。 如果我明確調用我的Release()覆蓋,則永遠不會調用它。

我真的很喜歡它,所以當我的原生Bar :: IFoo對象超出范圍時,它會自動調用我的.NET對象的dispose代碼。 我想我可以通過覆蓋Release()來實現它,如果對象count = 0則調用MyDispose()。 但顯然我沒有正確地覆蓋Release()因為我的Release()方法從未被調用過。

顯然,我可以通過在接口中放置MyDispose()方法並要求使用我的對象的人在Release()之前調用MyDispose()來實現這一點,但是如果Release()只是清理了對象,它會更加流暢。

是否有可能在釋放COM對象時立即強制調用.NET COM對象的析構函數或其他方法?

谷歌搜索這個問題讓我有很多命中,告訴我調用System.Runtime.InteropServices.Marshal.ReleaseComObject(),當然,這就是告訴.NET釋放COM對象的方法。 我想要COM Release()來處理.NET對象。

我有一個用托管代碼(C ++ / CLI)編寫的COM對象。 我在標准C ++中使用該對象。 當COM對象被釋放時,如何強制立即調用COM對象的析構函數? 如果那是不可能的,我可以讓Release()在我的(托管的DotNet - GBG)COM對象上調用Dispose()(而不是MyDispose() - GBG)方法嗎?

RE:當客戶端是非托管代碼時,強制確定性釋放由DotNet COM Server綁定的資源。 這些資源可以安排在垃圾收集器收集項目時釋放,但這不是確定性的,並且對於垃圾收集不頻繁的大型內存系統,文件流等資源可能無法釋放數小時或數天。

這是COM Callable Wrappers(CCW)的一個常見問題,可以通過另一個有點相關的線程看到: 是否可以攔截(或意識到)COM引用計數暴露給COM的CLR對象 在這種情況下,無論是在編寫自己的COM客戶端的任何情況下,無論是在托管代碼還是非托管代碼下,只需通過調用IDisposable.Dispose()方法就可以輕松解決。 但是,該方法不適用於(例如)一個DotNet COM編解碼器類,其客戶端可能是操作系統本身,哪個客戶端不應該知道COM服務器是非托管或托管的(DotNet)。

可以根據MSDN鏈接在DotNet COM服務器上實現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(),如果根據上述模板實現,則只能在第一次調用時有效地執行任何操作。 微軟可能會擔心一個類被廢棄,而仍有托管的引用,它不會知道它被處置,直到開始嘗試使用已經處置的資源拋出異常,但這是任何不當的潛在問題使用IDisposable接口,即使只有DotNet客戶端:如果有多個對同一對象實例的引用,並且其中任何一個都調用Dispose(),則其他人會發現嘗試使用所需的已處置資源導致異常。 對於這種情況,應該總是使用處理布爾值(根據IDisposable模板模板)放入警衛,或者僅通過公共包裝器引用對象。

由於Microsoft在mscoree.dll的CCW實現中沒有完成所需的幾行代碼,我在mscoree.dll周圍編寫了一個包裝器,增加了這個額外的功能。 它有點復雜,為了控制任何DotNet COM類的任何實例的包裝器的創建,我還需要包裝IClassFactory接口並在我的“CCW_Wrapper”包裝器類中聚合CCW實例。 此包裝器還支持來自另一個外部類的更多聚合級別。 該代碼還引用了正在使用的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中。 有誰知道如何聯系微軟內部相關部門的某人來提出這個建議?

EDITADD:我已經向Microsoft Connect提交了DotNet Framework的建議 這似乎是提供Microsoft反饋的最佳方式。

EDITADD2:在實現此問題的解決方法時,我意識到為什么MIcrosoft可能不會實現這個“自動調用Dispose,如果支持,當CCW引用計數降至零”時。 在編寫變通方法時,我必須獲取一個指向托管對象上COM接口的引用指針,以便將其傳遞給純粹的非托管COM方法,然后必須釋放引用計數的Release()以便不強制使用CCW引用該對象,從而導致內存泄漏,因為它永遠不可用於垃圾回收。 我之所以這樣做是因為我知道在托管對象上將引用計數減少到零目前只會使CCW刪除對該對象的強引用,如果沒有其他引用則使其符合垃圾回收的條件。 但是,如果Microsoft按照我的建議實現了Auto Dispose修補程序,或者如果此代碼包含mscoree.dll功能,則會在不需要時觸發托管對象上的Dispose()。 對於這種特殊情況,我可以“保護”Dispose(bool disposing)虛方法以防止Dispose()發生, 但對於任何使用此行為的現有代碼都采用相同的假設,包括Microsoft的DotNet Runtime Libraries的實現,實現CCW上的這個“修復”會破壞現有的代碼 這個包裝修復程序仍然適用於自己編寫的COM服務器,並且知道這種副作用,因為它們可以在Dispose()上放置“警衛”。

EDITADD 3:在進一步的工作中,我看到我對Microsoft的建議仍然有效,可以通過在實現的對象實例上調用IDisposable.Dispose()方法的修復來避免“打破”現有代碼的問題。管理COM服務器,如果僅當新的自定義屬性(如[AutoComDispose(true)](默認值為false)應用於托管COM服務器類時,該接口才存在。 通過這種方式,程序員可以選擇實現該功能,並且新屬性的文檔會提醒其使用有關必須“保護”Dispose()方法,例如當有可能存在“人工引用計數”時Marshal.Release()方法由托管服務器使用的代碼顯式調用,或者通過調用Marshal.GetObjectForIUnknown()等方法隱式調用,在某些情況下,如果ComObject是托管的,則可以調用QueryInterface和Release參考點賓語。

如上所述,本答案的主要問題是安裝使用的復雜性。

實際上,當最后一個引用被釋放時,不會從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();

但這是一個瘋狂的想法。

聲明為VS 2010(GBG)更正的對象(C ++ / CLI)的代碼:

using namespace System;
using namespace System::Runtime::InteropServices;

namespace Bar {

        [Guid("57ED5388-blahblah")]
        [InterfaceType(ComInterfaceType::InterfaceIsIDispatch)]
        [ComVisible(true)]
        public interface class IFoo
        {
                void Doit();
        };

        [Guid("417E5293-blahblah")]
        [ClassInterface(ClassInterfaceType::None)]
        [ComVisible(true)]
        public ref class Foo : IFoo
        {
        //these don't need to be seen...
        private:
            void DisposeManaged() {};
            void DisposeUnmanaged() {};
            ~Foo() {DisposeManaged(); this->!Foo();} // Only called by Dispose() on object instance or direct call and delete in C++/CLI
            !Foo() {DisposeUnmanaged();} // Called by the garbage collector and by the above.
        public:
        //THE FOLLOWING IS WRONG, ONE CANNOT OVERRIDE THE HIDDEN IUNKNOWN RELEASE() METHOD IN THIS WAY!!!
//      virtual ULONG Release() {MyDispose(); return 0;} // This is never called automatically!!!
            [PreserveSig];
            virtual void Doit() {};
        };
}

代碼更正如下:

  1. Release方法不會覆蓋CLI編譯器一無所知的隱藏IUnknown :: Release()方法,如果它被更正為實際返回ULONG值,

  2. 建議~Foo()析構函數只調用!Foo()終結器,以避免重復它需要做的事情,即釋放非托管資源,

  3. 析構函數~Foo()應該處理托管和非托管資源,但是終結器!Foo()應該只處理這里實現的非托管資源,

  4. 除了實現的接口方法之外,不需要公開任何方法

  5. 所有接口方法都應該[PreserveSig]以實現與COM的最大兼容性。

使用對象(本機C ++)的代碼已針對VS 2010(GBG)進行了更正,更正的注釋如下所示( 請注意,這包含了在編寫COM客戶端時問題的答案! ):

    #import "..\\Bar\\Bar.tlb" //raw_interfaces_only

    //C++ definition of the managed IDisposable interface...
    MIDL_INTERFACE("805D7A98-D4AF-3F0F-967F-E5CF45312D2C")
        IDisposable : public IDispatch
    {
    public:
        virtual VOID STDMETHODCALLTYPE Dispose() = 0;
    }

    ...
    CoInitialize(NULL);
    ...
        //the syntax for a "Smart Pointer" is as follows:
        Bar::IFooPtr pif(__uuidof(Bar::Foo)); // This object comes from the .tlb.
        if (pif)
        {
            //This is not stack based so the calling syntax for an object instance is as follows:
            pif->Doit();
            //THE FOLLOWING ANSWERS THE QUESTION: HOW TO DISPOSE ON RELEASE:  when one controls the COM client!!!
            IDisposable *id = nullptr;
            if (SUCCEEDED(pif->QueryInterface(__uuidof(IDisposable), (LPVOID*)&id)) && id)
            {
                id->Dispose();
                id->Release();
            }
            //The Release on the IUnknown is absolutely necessary, as without it the reference count stays as one!
            //This would result in a memory leak, as the Bar::Foo's destructor is never called,
            //and knows nothing about the IUnknown::Release() even if it were!!!
            pif->Release(); // explicit release, not really necessary since Bar::IFoo's destructor will call Release().
        }
    ...
    CoUninitialize();
    ...

似乎問題揭幕者並不真正理解COM服務器的Release引用計數方法與托管代碼中引用計數的絕對缺失之間的關系,而不是CCW模擬的:

如果我在我的COM對象上放置析構函數方法,則永遠不會調用它。 如果我放置一個終結器方法,它會在垃圾收集器到達時調用它。 如果我明確調用我的Release()覆蓋,則永遠不會調用它。

上面已經解釋了~Foo()析構函數和!Foo()終結器行為; 所謂的Release()覆蓋永遠不會被調用,因為它不是對任何東西的覆蓋,尤其是CCW提供的隱藏的IUnknown接口。 但是,這些編碼錯誤並不意味着次要問題沒有價值,並且有一些變通方法可以實現,正如我在其他答案中所述。

您可以通過IDisposable和Finalize來完成此操作。 weblogs.asp.net/cnagel/archive/2005/04/27/404809.aspx

這個答案沒有直接回答這個問題,因為IDisposable和Finalize已經在C ++ / CLI~Foo()和!Foo()上實現了; 提問者只是不明白如何調用我在上面顯示的Dispose()方法。

戈登,

我對這個答案很感興趣。 你的實施沒有運氣嗎?

查理

查理,我認為你最感興趣的是我的答案以及下面的2.或3. 我的答案1.正如我所描述的那樣工作2.但是,存在嚴重的問題,而不是實現3.但是為了使其保持工作,因為在檢查垃圾收集的可用性時,某些事件未修補補丁,即使COM對象不符合收集條件。 方法如下,但我已經找到了一種方法來使用DotNetFramework版本4解決3的主要問題:

  1. 簡潔的答案顯示了如何從用C ++編寫的本機非托管客戶端調用IDisposable.Dispose(),它回答了直接問題。 當然這是有效的,只有原始問題撰稿人在為托管COM寫作時缺乏經驗才能使他感到困難。 但是,這是否可以自動完成的次要問題是我在這里真正回答的問題。

  2. 使用C ++包裝器為COM可調用包裝器(CCW)解決方法,該解決方案由mscoree.dll中的Microsoft COM運行時執行環境實現。 這可以發布,歡迎您使用它,主要限制如下所示:您需要在系統目錄中安裝DLL文件(如果在64位系統上,則需要安裝它們),並且需要重新編寫InProcServer32的注冊表項鏈接到此包裝器DLL(同樣適用於32位和64位版本的注冊表)。 DLL文件也應進行數字簽名,以便某些內置Microsoft COM客戶端(如Windows Imaging Component(WIC))可以接受,以便完全正常運行。

  3. 另一種解決方法純粹來自托管代碼端,它通過“修補”/“掛鈎”/“調配”CCW接口的虛擬方法表來控制並添加自動調用IDisposable :: Dispose()所需的功能。當CCW提供的偽引用計數減少為零時的派生類。 這看起來它將工作,因為我有它為非聚合實例化情況工作,但我試圖支持聚合,然后我在這里提交它。 希望我能在幾天內完成它。 我用C#編寫,當然它也可以用托管/ CLI C ++編寫,最終將提供兩個實現的鏈接。 這比包裝器要復雜得多,但它的優勢在於它非常容易使用,因為我只需要對COM服務器類進行很少的更改就可以使用它,正如我將要展示的那樣,並且沒有額外的工作完全安裝。 這似乎是不可靠的,因為我需要做的修補因垃圾收集掃描中的某些事件而被撤消,因此我無法控制對引用計數的跟蹤。

因此,優點是易於使用。 但是,有一些缺點如下:

  1. 代碼通過在模擬的托管接口的情況下直接寫入虛擬方法表來修補虛擬方法表,這已經能夠在過去的許多年中完成但是沒有保證微軟未來可能不會改變它用於新版操作系統的內存模型。 我已嘗試對非托管內存中的虛擬方法表的副本進行更改,但無法輕松獲得某些關鍵功能 - Marshal.GetObjectForIunknown()方法調用不再按預期工作,這需要一些非常混亂的靜態查找表...我目前沒有使用它並直接修補托管vtable。 如果Microsoft改變了寫入限制,我可能會使“混亂”方法起作用。 除了使用DotNetFramework 4.0版提供的新功能外,看起來不可能。

  2. 為了保持相當簡單,我目前修改了QueryInterface實現掛鈎,以便忽略“額外”IMarshal ... IConnectionContainer接口,並且無法從COM訪問。 如果這些是必需的,那么可以在托管代碼中實現所有必需的接口 - 相當大的工作 - 或者找到一種方法來“掛鈎”這些接口,這可能是因為我已經“掛鈎”內部IUnknown非托管接口這是支持聚合所必需的。 但是,仍然是一項很大的編程任務。

  3. 由於非托管代碼和托管代碼之間的所有編組,代碼可能不是很快,並且可能不適合托管COM服務器,其中對COM服務器方法的調用快速且頻繁並且“細粒度”意味着每個調用都沒有做得那么多。 如果所需的托管COM服務器可以通過很少的方法調用完成相當多的工作,那么它應該沒問題,並且在托管代碼而不是非托管C ++中實現它的性能成本應該是最小的。

這個項目更多的是一個“白騎士”黑客項目的性質,在當前的實施工作!

完成之后,我應該把它作為一篇文章提交到某個網站,例如“代碼項目”,因為我認為它會對許多人有用。

無論如何,再觀看這個空間使用DotNetFramework 4.0中的設備...

在我看來,有三種方法可以解決這里提出的問題,具體如下:

  1. 當一個人直接控制本機非托管客戶端時,可以直接調用IDisposable.Dispose(),如我在Answer上清楚顯示的那樣; 這是最簡單的解決方案,但並不總能控制客戶端。

  2. 根據另一個問題,當CCW引用計數被釋放為零時,如何自動調用Dispose()方法的第二個問題的答案,我已經證明可以用COM可調用包裝器(CCW)的非托管代碼實現包裝一個非托管代碼DLL,以支持此功能。 這個解決方案並不難寫,但很難安裝。

  3. 在之前的一篇答案中,我試圖通過使用“修補”/“黑客攻擊”/“掛鈎”/“調配”技術來提出上述第二個問題的解決方案的第三種替代方案,以便托管代碼可以完全支持該功能,因此,除了托管的DotNet COM服務器通常所需的安裝之外,不需要任何額外的安裝。 我已經完全放棄了這個解決方案,因為在完成它之后,包括檢測和實現對客戶端聚合的支持,我發現鏈接到垃圾收集掃描進程的某些事件導致修補的指針未修補,這意味着我的例程失去控制跟蹤參考計數,從而跟蹤處理過程。

  4. 在這里,我使用新的ICustomQueryInterface接口提供了對次要問題的最后一個答案,該接口僅適用於DotNetFramework 4.0版 (可能更新),這避免了必須對現有虛擬方法表進行大部分風險修補。 使用這個新接口,仍然可以創建虛擬方法表,但只需要關注控制僅在客戶端聚合COM服務器時使用的內部IUnknown接口。 該方法有一個輕微但不嚴重的限制,我將在實施后介紹。

第二個問題實際上與為什么在COM引用計數器變為零時不會自動調用與~Foo()析構函數關聯的實際Dispose()的原因有關,原因是COM未實現此功能可調用包裝器(CCW),至少目前不是。 需要回答的第二個問題與以下問題有關:當一個人沒有編寫COM客戶端時,一般情況下哪些客戶端是在假定非托管本機代碼COM服務器的情況下編寫的,但COM服務器需要確定性地處理資源,如問題文本如下:

我真的很喜歡它,所以當我的原生Bar :: IFoo對象超出范圍時,它會自動調用我的.NET對象的dispose代碼。

在這里,提問者並不真正理解本機C ++沒有用於確定基於堆棧的變量何時“超出范圍”的機制,這與使用“堆棧語義”的C ++ / CLI不同,並且必須手動調用Release ()方法,如我所示。 因此,正確答案如下:

實際上,當最后一個引用被釋放時,不會從COM客戶端調用Dispose(或者我應該說~Foo)和Release。 它根本沒有實現。 這里有一些想法可以做到這一點。

http://blogs.msdn.com/oldnewthing/archive/2007/04/24/2252261.aspx#2269675

但即使是作者也不建議這種方法。

以上是正確的,非托管Com Callable Wrapper(CCW)不是IDisposable接口感知(由~Foo()C ++ / CLI語法自動隱含的接口),因此不調用(實際)Dispose()方法這將觸發OP(和其他所有人)的願望。 但是,有一些解決方法可以解決這個問題,因為我在這里做出了貢獻。

我考慮了“MadQ”表達的想法,按照頁面底部上面鏈接的頁面和他關於通過“掛鈎”/“調配”vTable指針在托管代碼中實現修復的想法,並認為這想法可能實際上有一些修改。 雖然這個想法可能不完全可取,因為人們在構建自己的vTable條目時有效地編寫自修改代碼,但它與CCW構建的接口代理沒有什么不同,以便能夠支持DotNet COM服務器。 “MadQ”概述的基本前提有很多遺漏和問題如下:

  1. 建議將IUnknown接口掛鈎或“調配”,以便能夠檢查引用計數何時降至零,但對於更常見的非聚合情況,所有其他接口也可以更改引用計數對象實例,包括所有托管接口以及CCW自動提供的非托管接口。 最后一個可以包括IMarshal,IProvideClassInfo,ISupportErrorInfo,可能分別是ItypeInfo和IErrorInfo中最后兩個的支持接口,IDispatch如果類使用AutoDispatch或AutoDual設置也將創建coclass接口,IConnectionPointContainer和IConnectionPoint如果COM事件是如果在托管類上實現IExpando接口,則實現IDispatchEx;如果托管類實現IEnumerable接口,則為IEnumVARIANT(可能存在其他未發現的接口)。

  2. 對於一旦檢測到非聚合的情況,可以為所有管理的Release()指針添加“swizzling”/掛鈎,正如為IUnknown提議的那樣; 但是,因為其他非托管接口是在非托管C ++中實現的,所以它們位於代碼空間中,並且無法編寫/修補vTable。 這可以通過在分配的Co任務內存中創建vTable的副本來解決,但是例程本身會檢測到對象已經與真正的vTable分離。 造成這種情況的解決辦法是使用對象實例的靜態字典和指針他們屬於使實際的對象可以抬起頭,然后修補接口枕able指針未打補丁,調用的方法,以及指針重新修補了每個非托管接口中的每個方法 - 很多非常混亂的工作,並且在非托管代碼和托管代碼之間來回編組的所有流程中都不太好。 此外,支持多線程這樣做存在很大的潛在問題,因此必須找到問題的其他解決方法。 因此,我試圖堅持修補托管接口,並且只使用用於聚合的內部IUnknown接口的新模擬虛擬方法表; 但是我遇到了下面4中描述的嚴重問題。

  3. 我在http://msdn.microsoft.com/en-us/library/aa720632(VS.71).aspx]的Microsoft文檔中注意到.Net類可以提供這些接口的實現,這些接口覆蓋這些本機代碼實現,所以所有接口都可能看起來是托管類,實現似乎取代了非托管接口。 這是一個可行的解決方案,但如果需要,在托管代碼中實現所有這些接口需要大量工作。

  4. 或者,可以修改QueryInterface方法,以便生成的托管COM服務器不支持非托管接口,這在最初的問題中是最需要的。 實際上,這使得上述非托管的接口實現完全沒有問題,因為QueryInterface()的鈎子化托管代碼版本只能響應已實現的托管類,並拒絕對那些不需要或實現為托管版本的其他接口的任何查詢的接口。 這是使用低於4.0版的DotNetFramework實現此操作的主要限制,除了垃圾收集器取消修補/恢復虛擬方法表之外,如果修補了原始IUnknown接口,則無法再使用Marshal.GetObjectForIUnknown()和Marshal。此服務器上的GetUniqueObjectForIUnknown(),如果將DotNet托管的COM服務器作為參數傳遞給其他托管代碼的另一個托管COM服務器對象,則必須這樣做。 例如,非托管Windows映像組件(WIC)編解碼器系統的某些部分可能會將托管的IBitmapDecoder對象的實例傳遞給托管的WPF代碼,這將通過運行時可調用包裝器(RCW)從此類調用方法失敗,除非這些Marshal方法有效。 我沒有成功地讓系統認為實現是一個非托管接口,可以支持完整RCW和非托管代碼。

  5. 控制這種非托管“額外”接口的另一種替代方法是將這些接口的托管實現編寫為僅調用已經在CCW中實現的非托管代碼,如果需要,這可能是控制這些接口的最簡單方法。

  6. 最后,“MadQ”錯過了一個技巧,因為CCW引用計數增加並且在鏈接被“掛鈎”之前減少到零沒有任何問題,因為它不會導致C ++類型刪除並且只取消強引用托管對象實例使其不被確認和垃圾收集。 這樣,一個不依賴於托管類只能用於非托管COM客戶端,因為至少有一個的引用計數從不允許它為.Net客戶端進行垃圾收集,因為在構造時引用計數可以保持為零盡管有一些對各種實例接口的引用。

這項技術有什么好處如下:

這種替代解決方法完全來自托管代碼端,通過“修補”/“掛鈎”/“調配”CCW接口的虛擬方法表來實現控制並添加自動調用IDisposable.Dispose()所需的功能。當CCW提供的偽引用計數減少為零時的派生類。 這比包裝器要復雜得多,但它的優勢在於它非常容易使用,因為我會對COM服務器類進行很少的更改以便像我將要展示的那樣使用它,並且沒有額外的安裝工作一點都不

因此,優點是易於使用。 但是,有一些缺點如下:

  1. 即使使用DotNet 4.0提供的新ICustomQueryInterface接口,也無法控制/跟蹤對象實例的正常非聚合IUnknown接口,並且可以防止此接口上的查詢找到任何其他不希望公開的接口對於COM,無法跟蹤通過此接口完成的引用計數。 幸運的是,似乎可以控制對通常隱藏的內部IUnknown接口的引用計數的跟蹤,該接口僅在客戶端聚合對象時使用。

  2. 為了保持相當簡單,我目前修改了QueryInterface實現掛鈎,以便忽略“額外”IMarshal ... IConnectionContainer接口,並且無法從COM訪問。 如果需要這些,可以在托管代碼中實現所有必需的接口 - 相當大的工作 - 或者找到一種方法來“鈎住”這些接口,這也可以通過少量的額外代碼和研究來完成。

  3. 由於非托管代碼和托管代碼之間的所有編組,代碼可能不是很快,並且可能不適合托管COM服務器,其中對COM服務器方法的調用快速且頻繁並且“細粒度”意味着每個調用都沒有做得那么多。 如果所需的托管COM服務器可以通過很少的方法調用完成相當多的工作,那么它應該沒問題,並且在托管代碼而不是非托管C ++中實現它的性能成本應該是最小的。

這個項目更具有“白騎士”黑客攻擊項目的性質,可以解決當前的實施問題!

與Question相反,要啟動,非托管COM客戶端的實現只會更改為不需要調用IDisposable.Dispose()方法(就像包裝器Answer一樣),如下所示:

    #import "..\\Bar\\Bar.tlb" //raw_interfaces_only

    ...
    CoInitialize(NULL);
    ...
        //the syntax for a "Smart Pointer" is as follows:
        Bar::IFooPtr pif(__uuidof(Bar::Foo)); // This object comes from the .tlb.
        if (pif)
        {
            //This is not stack based so the calling syntax for an object instance is as follows:
            pif->Doit();
            //THE FOLLOWING IS NOT REQUIRED AND IS COMMENTED OUT!!!
            //IDisposable *id = nullptr;
            //if (SUCCEEDED(pif->QueryInterface(__uuidof(IDisposable), (LPVOID*)&id)) && id)
            //{
            //  id->Dispose();
            //  id->Release();
            //}
            //The Release on the IUnknown is absolutely necessary, as without it the reference count stays as one!
            //This would result in a memory leak, as the Bar::Foo's destructor is never called,
            //and knows nothing about the IUnknown::Release() even if it were!!!
            pif->Release();
        }
    ...
    CoUninitialize();
    ...

接下來,對托管COM服務器的唯一更改是使COM服務器類從新的AutoComDisposenC基類繼承(直接或間接)。 我還添加了“保護”析構函數代碼的附加功能,使其僅在第一次被調用時才有效,因為這些文件只關閉一次,等等,如下所示:

using namespace System;
using namespace System::Threading;
using namespace System::Runtime::InteropServices;

namespace Bar {

        [Guid("57ED5388-blahblah")]
        [InterfaceType(ComInterfaceType::InterfaceIsIUnknown)]
        [ComVisible(true)]
        public interface class IFoo
        {
            void Doit();
        };

        [Guid("417E5293-blahblah")]
        [ClassInterface(ClassInterfaceType::None)]
        [ComVisible(true)]
        public ref class Foo : AutoComDisposenC, IFoo
        {
        //these don't need to be seen...
        private:
            bool disposed;
            Object ^disposedlock;
            void DisposeManaged() {};
            void DisposeUnmanaged() {};
            ~Foo() // called directly or by Dispose() on this instance
            {
                //multi-threading safe, only effective first time it's called...
                bool d;
                Monitor::Enter(this->disposedlock);
                    //in critical section = only one thread at a time
                    d = this->disposed;
                    this->disposed = false;
                Monitor::Exit(this->disposedlock);
                if (!d)
                {
                    DisposeManaged();
                    this->!Foo();
                }
            }
            !Foo() {DisposeUnmanaged();} // This is called by the garbage collector and the above.
        public:
            Foo() : disposed(FALSE), disposedlock(gcnew Object()) {};
            //THE FOLLOWING IS WRONG AS ONE CANNOT OVERRIDE THE HIDDEN IUNKNOWN RELEASE() METHOD!!!
//      virtual ULONG Release() {MyDispose(); return 0;} // This is never called automatically!!!
            [PreserveSig]
            [ComVisible(true)]
            virtual void Doit() {};
        };
}

最后,將來,將是AutoComDisposenC類的實際實現:

完成后,我應該將這個備選答案作為文章提交給某些網站,例如“代碼項目”,因為我認為這對許多人來說會有用。

如果沒有用C ++創建包裝器(沒有.NET),我不知道如何。 問題是,當對Release的調用將COM引用計數降為0時,.NET Framework不知道是否仍有未包含在COM引用計數中的托管引用。

基本限制是.NET沒有只能從COM訪問的對象的概念。 並且由於.NET對象的引用在垃圾收集之前無法確定,因此在發布時沒有確定的處理方式,就像在純COM中一樣。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM