如何在Release()上处理.NET COM互操作对象

我有一个用托管代码(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 "..DebugFoo.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()来完成它,如果对象计数= 0,则调用MyDispose()。 但显然我并没有正确地重写Release(),因为我的Release()方法从未被调用过。

显然,我可以通过在接口中放置我的MyDispose()方法并要求使用我的对象的人在Release()之前调用MyDispose()来实现这一点,但如果Release()只是清理对象,它将变得更加光滑。

当COM对象被释放时,是否可以强制.NET COM对象的析构函数或其他方法立即被调用?

在这个问题上用谷歌搜索得到我很多命中告诉我要调用System.Runtime.InteropServices.Marshal.ReleaseComObject(),但当然,这就是你如何告诉.NET释放COM对象。 我想要COM Release()来处理一个.NET对象。


实际上Dispose(或者我应该说〜Foo)不是释放将在最后一个引用被释放时从COM客户端调用。 它根本没有实现。 这里有一些想法可以做到这样的事情。

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对象的析构函数被立即调用? 如果这是不可能的,我可以在我的(托管的DotNet - GBG)COM对象上使用Release()调用Dispose()(不是MyDispose() - GBG)方法吗?

RE:当客户端是非托管代码时,强制确定性释放由DotNet COM服务器绑定的资源。 当垃圾收集器收集该项目时,可以安排这些资源被释放,但是这不是确定性的,并且对于垃圾收集不频繁的大内存系统,文件流可能不会释放数小时或数天。

这是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调用。 理想情况下,在mscoree.dll中实现CCW应该实际检查并调用IDisposable.Dispose()方法(如果作为CCW版本和/或finalization / destructor的一部分实现的话)。 我不确定为什么微软没有这样做,因为有完全访问程序集的信息,他们可以很容易地确定DotNet COM类是否支持IDisposable,如果是最终版本,只需调用Dispose(),并且因为这将在CCW可以避免由于额外的接口引用而导致的处理引用计数的所有复杂性。

我看不出这将如何“破坏”任何现有的代码,因为任何知道IDisposable的客户端仍然可以调用Dispose(),如果按照上述模板实现,只会在第一次调用时有效地执行任何操作。 微软可能会担心一个类正在被处置,而仍然有一个被管理的引用,它不知道它是否被抛弃,直到通过尝试使用已经处置的资源开始抛出异常,但这是任何不当的资源的潜在问题即使只有DotNet客户端也使用IDisposable接口:如果有多个对同一个对象实例的引用,并且其中任何一个对象调用Dispose(),其他对象将尝试使用所需的处置资源导致异常。 对于这种情况,应该总是使用disposing布尔值(根据IDisposable模式模板)放置警戒者,或者只通过通用包装器引用对象。

由于Microsoft在mscoree.dll的CCW实现中没有完成所需的几行代码,因此我在mscoree.dll上编写了一个包装,它增加了这个额外的功能。 这有点复杂,因为为了控制围绕任何DotNet COM类的任何实例创建包装,我还需要包装IClassFactory接口并将CCW实例聚合到我的“CCW_Wrapper”包装类中。 这个包装器还支持来自另一个外部类的进一步级别的聚合。 该代码还对使用的mscoree.dll实现内的类实例进行引用计数,以便在没有引用(以及稍后有必要时再次LoadLibrary)时能够在mscoree.dll上调用FreeLibrary。 该代码也应该是多线程友好的,如在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位版本放入系统文件夹,并将32位版本放入SysWOW64文件夹; 同样,正常的CLSID注册和虚拟化的WOW6432版本都应该修改为InprocServer32条目。 某些应用程序可能需要这个包装器DLL被数字签名才能无缝工作,这是一个完整的其他主题。 如果有人想要,我会链接到这些DLL的编译版本。

正如我所说,所需的几行(不包括包装要求)技术应该真正纳入mscoree.dll。 有没有人知道如何联系微软内部相关部门的人员提出这个建议?

EDITADD:我已经向Microsoft Connect提交了DotNet Framework的建议。 这似乎是提供微软反馈的最佳方式。

EDITADD2:在解决这个问题时,我意识到为什么微软可能不会实现这个“自动调用Dispose,如果支持,当CCW引用计数下降到零”。 在编写解决方法时,我必须获得一个引用指向受管对象上的COM接口的指针,以便将其传递给纯粹的非托管COM方法,然后必须使用Release()引用计数才能无法强制执行CCW引用该对象,因此永远不会将其用于垃圾回收,从而导致内存泄漏。 我这样做是因为我知道在当前托管对象上将引用计数减为零只会使CCW放弃对该对象的强引用 ,使其有资格进行垃圾回收(如果没有其他引用)。 但是,如果Microsoft按照我的建议实施了自动配置修补程序,或者如果此代码包装了mscoree.dll功能,则在不需要时会在托管对象上触发Dispose()。 对于这种特殊情况,我可以“守护”Dispose(bool disposing)虚拟方法来防止Dispose()的发生, 但对于任何现有的代码来说,它们都以相同的假设使用此行为,包括Microsoft的DotNet Runtime Libraries实现, CCW上的这个“修复”会破坏现有的代码 。 这个包装器修复仍然适用于COM服务器自己写的并知道这种副作用,因为他们可以将“保护”置于Dispose()上。

EDITADD 3:在进一步的工作中,我看到我对微软的建议仍然有效,并且可以通过修复程序来避免“破坏”现有代码的问题,该修复程序将在对象实例上调用IDisposable.Dispose()方法如果接口仅在默认值为false的新自定义属性(如[AutoComDispose(true)])应用于托管COM服务器类时才存在,则为托管COM服务器 。 通过这种方式,程序员可以选择实现这些功能,并且关于新属性的文档可能会提醒其使用有关必须“保护”Dispose()方法的用法,例如在存在Marshal.Release()方法在托管服务器使用的代码中显式调用,或者隐式调用Marshal.GetObjectForIUnknown()方法,在某些情况下,如果ComObject是托管服务器,可以调用QueryInterface和Release参考点目的。

如上所述,这个答案的主要问题是安装它的复杂性。


声明对象的代码(C ++ / CLI)已针对VS 2010(GBG)进行了更正:

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() {};
        };
}

代码纠正如下:

  • Release方法不会覆盖CLI编译器一无所知的隐藏的IUnknown :: Release()方法,如果它被更正为实际返回一个ULONG值,

  • 建议〜Foo()析构函数调用!Foo()终结器,以避免重复它需要做的事情,即释放非托管资源,

  • 析构函数〜Foo()应该处理托管和非托管资源,但是终结器!Foo()应该只处理这里实现的非托管资源,

  • 除了已实现的接口方法以外,没有必要公开任何方法

  • 所有的接口方法都应该做成[PreserveSig],以便与COM最大的兼容性。

  • 使用该对象的代码(本机C ++)已针对VS 2010(GBG)进行了更正,更正的注释如下( 注意,这包含了在编写COM客户端时问题的答案! ):

        #import "..BarBar.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服务器的发布引用计数方法与托管代码中绝对不存在引用计数之间的关系,而不是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()方法。

    链接地址: http://www.djcxy.com/p/81305.html

    上一篇: How to dispose of a NET COM interop object on Release()

    下一篇: How to implement an outgoing interface on a sink object (C++)