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

mhe*_*384 10 .net com interop release c++-cli

我有一个用托管代码(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对象.

Gor*_*ood 11

我有一个用托管代码(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;
}
Run Code Online (Sandbox Code Playgroud)

DLL还需要'.def'文件,如下所示:

LIBRARY mscoreeCOM_DisposeWrapper

EXPORTS
    DllCanUnloadNow         PRIVATE
    DllGetClassObject       PRIVATE
Run Code Online (Sandbox Code Playgroud)

要使用此源代码,将其编译为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参考点宾语.

如上所述,本答案的主要问题是安装使用的复杂性.


小智 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}
Run Code Online (Sandbox Code Playgroud)

我能想到的其他选择是实现某种自己的“COM 垃圾收集器”。COM 创建的每个对象都将放置在一个列表中(前提是您的类型的对象只能由 COM 创建 - 我想不出任何方法来区分创建对象的位置)。然后您必须定期检查列表,并在每个对象上调用如下内容:

IntPtr iUnk = Marshal.GetIUnknownForObject(@object);
int refCount = Marshal.Release(iUnk);
if (refCount == 0)
    @object.Dispose();
Run Code Online (Sandbox Code Playgroud)

但这是一些疯狂的想法。