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

10
我有一个用托管代码(C++/CLI)编写的COM对象,我正在标准C++中使用该对象。如何在COM对象被释放时立即调用析构函数?如果不可能的话,是否可以在Release()上调用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();} // 这个从未被调用。 !Foo() {MyDispose();} // 垃圾回收器调用此函数。 virtual ULONG Release() {MyDispose();} // 这个从未被调用。 }; ```
使用对象的代码(本机C++):
``` #import "..\\Debug\\Foo.tlb" ... Bar::IFoo setup(__uuidof(Bar::Foo)); // 此对象来自 .tlb 文件。 setup.Doit(); setup->Release(); // 显式释放, 实际上并不是必要的,因为 Bar::IFoo 的析构函数将调用 Release() 。 ```
如果我在我的 COM 对象上放置析构函数,则永远不会调用它。如果我放置最终器函数,则垃圾回收器会在适当时候调用它。如果我显式调用我的 Release() 重载函数,则从未调用它。
我希望在本机 Bar::IFoo 对象的作用域超出范围时自动调用我的 .NET 对象的释放代码。我认为我可以通过重载Release()方法来实现这一点,如果对象计数=0,则调用MyDispose()。但显然我没有正确地重载Release(),因为从未调用我的Release()方法。

显然,我可以通过将MyDispose()方法放在接口中,并要求使用我的对象的人在调用Release()之前调用MyDispose()来实现这一点,但如果Release()能够清理对象就更好了。

是否有可能强制.NET COM对象的解构函数或其他方法在COM对象被释放时立即被调用?

在Google上搜索此问题会得到很多提示要求调用System.Runtime.InteropServices.Marshal.ReleaseComObject(),但当然,那是告诉.NET释放COM对象的方法。 我想让COM Release()处理.NET对象的Dispose。

6个回答

14
我有一个用托管代码(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;

//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;
}

一个'.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。

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


Gordon,我对这个问题的答案非常感兴趣。你在实现上有什么进展吗?Charlie - user565780
Charlie,这个答案可行,但需要注意以下限制:您需要将DLL文件安装在系统目录中(如果是64位系统,则需要在两个系统目录中都安装),并且需要重新编写InProcServer32的注册表项以链接到此包装器DLL(也需要为注册表的32位和64位版本都进行操作)。 DLL文件还应数字签名以被一些内置的Microsoft COM客户端(如Windows Imaging Component(WIC))接受,以便完全功能。 - GordonBGood
Charlie,我想到你的评论可能被移动到了错误的答案下面,而且你更感兴趣的是托管代码解决方案。如果是这样,很快就会更新一个可行的版本作为另一种选择的答案。 - GordonBGood

6

实际上,当最后一个引用被释放时,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();

但这是一些疯狂的想法。

1

声明对象的代码(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() {};
        };
}

代码已经被更正如下:

  1. Release方法没有覆盖隐藏的IUnknown :: Release()方法,CLI编译器对此一无所知,如果它被更正为实际返回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()方法,我已经在上面展示了如何调用。


1

在我看来,解决这里提出的问题有三种方法,如下:

  1. 当一个人直接控制本地未托管客户端时,可以像我在一个答案中清楚地展示的那样直接调用IDisposable.Dispose(),这是最简单的解决方案,但并不总是能够控制客户端。

  2. 针对如何使Dispose()方法在CCW引用计数降为零时自动调用的另一个问题,我已经展示了一种方法,即使用未托管代码DLL包装COM Callable Wrappers(CCW)的未托管代码实现来支持此功能。这个解决方案不难编写,但安装起来比较困难。

  3. 在之前的一个答案中,我试图提出第三种解决上述次要问题的替代方案,即使用“修补”/“黑客”/“挂钩”/“交换”技术,以便完全由托管代码支持该功能,因此不需要除托管DotNet COM服务器通常所需的任何额外安装。我几乎放弃了这个解决方案,因为在完成它后,包括检测和实现对客户端聚合的支持,我发现与垃圾回收扫描过程相关联的某个事件会导致修补的指针被取消修补,这意味着我的例程失去了跟踪引用计数和处理过程的控制。

  4. 在这里,我提供了使用新的ICustomQueryInterface接口的最后一个答案,该接口仅适用于DotNetFramework版本4.0(以及可能更新的版本),它避免了大部分现有虚拟方法表的风险性修补。使用这个新接口,仍然需要创建虚拟方法表,但只需要关注客户端聚合时才使用的内部IUnknown接口的控制。该方法有一个轻微但不严重的限制,在实现被介绍之后将进行讨论。

次要问题实际上与为什么当COM引用计数器归零时,与~Foo()析构函数相关的Dispose()不会自动调用有关,原因是这个功能目前由COM Callable Wrapper (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()方法,这将触发所有人所期望的一切。然而,有一些解决方法可以解决这个问题,就像我在这里提供的一样。

我开始思考“MadQ”在上面链接页面底部所表达的想法,他提出了通过“hooking”/“swizzling” vTable指针在托管代码中实现修复的想法,并且认为这个想法可能会在一些修改后实际起作用。虽然这个想法可能并不完全可取,因为实际上是在构建自己的vTable条目时编写自修改代码,但它与CCW构建的接口代理以支持DotNet COM服务器并没有什么不同。以下是“MadQ”所概述的基本前提存在相当多的遗漏和问题:

提议对IUnknown接口进行挂钩或“swizzled”,以便能够检查引用计数何时降至零,但对于更常见的非聚合情况,所有其他接口也可以更改对象实例的引用计数,包括所有托管接口以及由CCW自动提供的非托管接口。这些最后可能包括IMarshal、IProvideClassInfo、ISupportErrorInfo,可能是这两个接口的支持接口在ItypeInfo和IErrorInfo中,如果类使用AutoDispatch或AutoDual设置,则还将创建一个coclass接口的IDispatch,IConnectionPointContainer和可能的IConnectionPoint如果实现了COM事件,如果在托管类上实现了IExpando接口,则为IDispatchEx,如果托管类实现了IEnumerable接口作为集合,则为IEnumVARIANT(可能存在其他未发现的接口)。
对于非聚合情况,一旦检测到,就可以像建议IUnknown那样为所有托管Release()指针添加“swizzling”/hooking;然而,由于其他非托管接口是在非托管C++中实现的,它们位于代码空间中,因此无法编写/修补vTable。这将通过在分配的Co任务内存中创建vTable的副本来解决,但然后例程本身会检测到对象已与真实的vTable分离。解决这个问题的方法是使用对象实例的静态字典和它们所属的指针,以便可以查找实际对象,然后取消修补接口vTable指针,调用方法,并重新修补指针,对于每个未管理的接口中的每个方法-非常混乱的工作,并且在非托管和托管代码之间来回传递时并不美好。此外,支持多线程存在重大潜在问题,因此必须找到解决该问题的其他解决方案。因此,我尝试仅为托管接口打补丁,并为聚合所用的内部IUnknown接口使用新的模拟虚拟方法表;但是,我遇到了下面描述的4个严重问题。
我注意到Microsoft文档中http://msdn.microsoft.com/en-us/library/aa720632(VS.71).aspx],.Net类可以提供自己的这些接口实现,覆盖这些本机代码实现,因此所有接口都可以出现为托管类,实现似乎替换了非托管接口。这是可行的解决方案,但如果需要,则需要在托管代码中实现所有这些接口,这需要大量的工作。
或者,可以仅对QueryInterface方法进行修补,以便由生成的托管COM服务器不支持非托管接口,这是最初的问题所需的全部。实际上,这使得上述接口的非托管实现成为完全无关紧要,因为挂钩的托管代码版本的QueryInterface()只能响应实现的托管类,并拒绝任何查询不需要或未实现为接口的托管版本。这是使用DotNetFramework版本小于4.0实现此功能的主要限制,因为除了垃圾收集器取消修补/恢复虚拟方法表之外,如果原始IUnknown接口被修补,则无法在此服务器上使用Marshal.GetObjectForIUnknown()和Marshal.GetUniqueObjectForIUnknown(),如果DotNet托管COM服务器将通过其他非托管代码作为参数传递给另一个托管COM服务器对象,则这是必要的。例如,非托管Windows Imaging Component(WIC)编解码器系统的

这种技术的好处如下:

这种替代方法完全是从托管代码方面实现的,通过“修补”/“挂钩”/“交换”CCW接口的虚拟方法表来控制并添加所需功能,即在它们的伪引用计数由CCW减少到零时自动调用IDisposable.Dispose()释放派生类。相比包装器,这更加复杂,但优点是非常容易使用,因为只需对COM服务器类进行很少的更改即可使用,我将展示如何使用,并且根本不需要额外的安装工作。

因此,它的优点是易于使用。然而,也有一些缺点:

  1. 即使使用DotNet 4.0提供的新ICustomQueryInterface接口,也无法控制/跟踪对象实例的正常非聚合IUnknown接口。虽然可以防止在此接口上进行的查询找到任何其他不想向COM公开的接口,但无法跟踪通过此接口执行的引用计数。幸运的是,似乎可以控制仅在客户端聚合对象时使用的通常隐藏的内部IUnknown接口的引用计数跟踪。

  2. 为了保持相对简单,我目前修改了QueryInterface实现钩子,以便忽略“额外”的IMarshal...IConnectionContainer接口,并且这些接口不能从COM访问。如果需要这些接口,则可以在托管代码中实现所有所需的接口-这是一项相当大的工作-或找到一种方法来“挂钩”这些接口,这也可以完成,但需要更少的额外代码和研究。

  3. 由于在非托管代码和托管代码之间进行了大量的封送处理,因此代码可能不太快,并且可能不适用于托管COM服务器,其中对COM服务器方法的调用快速而频繁,并且“细粒度”,这意味着每个调用并不做太多工作。如果所需的托管COM服务器可以使用少量方法调用完成大部分工作,则应该可以正常运行,并且将其实现为托管代码而不是非托管C++的性能成本应该很小。

这个项目更多地是一个“白骑士”黑客项目,旨在解决当前实现中的问题!

与问题相反,为了开始,未托管的COM客户端的实现只需更改,以不需要调用IDisposable.Dispose()方法(就像包装器答案一样),如下所示:

    #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类的实际实现:

完成后,我应该将这个替代答案提交给一些网站,比如"The Code Project",因为我认为它会对许多人有用。


本地C++没有一种机制来确定何时基于堆栈的变量会“超出作用域”,不像使用“堆栈语义”的C++/CLI。-- 什么让你这么想? C++/CLI堆栈语义旨在复制本地C ++行为的解构函数,这肯定会让程序员在变量超出作用域时执行清理操作。 请阅读有关RAII的内容。 或者您可能是想说C#没有这样的机制(尽管“using”块提供了有限的替代方案)? - Ben Voigt
@BenVoigt,我的意思是C#没有这样的机制,虽然块是有限的替代品,但这是一种尝试强制C#在已知时间执行销毁的练习。 - GordonBGood

1
Gordon,
我非常关心这个问题的答案。你在实现方面有什么好运吗?
Charlie Charlie,我想你主要对我的答案和2或3感兴趣。我的答案1就像我描述的那样工作,2也是如此。然而,有严重的问题,不是在实现3时,而是在保持它的工作状态时,因为检查垃圾回收的可用性时会取消修补程序,即使COM对象不符合收集条件。以下是方法,但我已经找到了一种使用DotNetFramework版本4来解决3个主要问题的方法:
  • 简洁明了地回答了一个问题:"如何从用 C++ 编写的本机非托管客户端调用 IDisposable.Dispose() 方法"。当然,这样做是有效的,对于那些刚接触托管 COM 编程的人来说,这可能会有些难度。但是我在这里真正回答的是第二个问题:是否可以自动完成此操作。

  • 使用 C++ 包装器绕过 Microsoft COM 运行时环境中的 COM Callable Wrapper (CCW) 的限制(该环境实现在 mscoree.dll 中)。这种方法已发布并且您可以使用它,但主要限制如下:您需要将 DLL 文件安装在系统目录中(如果是 64 位系统则需要安装两个文件),并且需要重新编写 InProcServer32 的注册表项以链接到此包装器 DLL(对于注册表的 32 位和 64 位版本都需要重新编写)。此 DLL 文件还应数字签名以便于一些内置的 Microsoft COM 客户端使用,例如 Windows Imaging Component (WIC),以实现其完全功能。

  • 另一种替代解决方案是完全从托管代码的角度实现的,它通过 "补丁" / "挂钩" / "重载" CCW 接口的虚方法表来控制并添加所需功能,使其自动调用派生类中的 IDisposable::Dispose() 方法,当它们的伪参考计数提供的 CCW 减少到零时。我已经在非聚合实例化情况下使其工作了,但是正在尝试在提交之前支持聚合。希望我会在几天内让它工作。我使用 C# 编写,但是当然也可以使用托管/CLI C++ 编写,并将最终提供两种实现的链接。这比包装器要复杂得多,但它的优点是非常容易使用,因为 COM 服务器类只需进行极少的更改即可使用它,并且根本不需要安装额外的工作。然而,这种方法并不可靠,因为垃圾收集过程中的某些事件会撤消我所需做的补丁,导致我无法跟踪引用计数。

因此,它的优点在于易于使用。然而,还存在以下一些缺点:

  1. 在模拟托管接口的情况下,代码通过直接写入虚方法表来修补它们。这已经可以做了很多年,但是不能保证微软不会在未来的操作系统新版本中更改内存模型。我尝试对非托管内存中的虚方法表副本进行更改,但无法轻松地使某些关键功能正常工作——Marshal.GetObjectForIunknown() 方法调用不再按预期工作,这需要一些非常混乱的静态查找表...目前我没有使用它,而是直接为托管的虚拟表打补丁。如果微软更改了写入限制,我可能可以让这种“混乱”的方法工作。 除了使用DotNetFramework 4.0提供的新功能外,似乎没有其他可能性。

  2. 为了保持相对简单,我当前修改了 QueryInterface 实现钩子,以便忽略“额外”的 IMarshal...IConnectionContainer 接口,并且这些接口无法从 COM 中访问。如果需要这些接口,则可以在托管代码中实现所有所需的接口(这是一个相当大的工作),或者找到一种方法来“挂钩”这些接口,这很可能是可能的,因为我已经“挂钩”了内部的 IUnknown 非托管接口,这是支持聚合所必需的。不过,这仍然是一个相当大的编程任务。

  3. 由于非托管代码和托管代码之间的所有封送处理,代码可能不太快,并且可能不适用于调用 COM 服务器方法的速度快且频繁且“细粒度”的托管 COM 服务器,这意味着每个调用并不做太多事情。如果所需的托管 COM 服务器可以使用少量的方法调用完成很多工作,则应该没问题,并且将其实现为托管代码而不是非托管 C++ 的性能成本应该是最小的。

这个项目更多地是一个“白帽子”黑客项目,旨在绕过当前的实现!
完成后,我应该将其作为一篇文章提交到一些网站,如“代码项目”,因为我认为它会对许多人有用。
无论如何,请关注这个空间,使用DotNetFramework 4.0的设施会有更多答案...

优秀的帖子,但请注意——如果它确实是问题的另一个答案,例如另一种技术,则应该成为另一个答案。 - Jeff Atwood
Jeff,我现在将这篇文章更改为回答的性质,尽管最后是一个失败的回答,因为在DotNetFramework < 4.0下纯粹的补丁/钩子/"swizzling"虽然有效,但由于垃圾收集扫描相关的未知事件触发系统取消打补丁,它并不可靠。我将编辑我的上一个回答来展示如何使用新的官方支持的ICustomQueryInterface接口在DotNetFramework版本4.0下实现最终结果,而且限制很少。 - GordonBGood

0

除了在 C++ 中创建一个包装器(不使用 .NET),我不知道还有其他方法。问题在于,当调用 Release 将 COM 引用计数降为 0 时,.NET Framework 不知道是否仍存在未包括在 COM 引用计数中的托管引用。

根本限制在于 .NET 没有只能从 COM 访问的对象的概念。由于无法确定来自 .NET 对象的引用直到垃圾回收,因此没有像纯 COM 中那样在释放时确定处理的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接