Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have a COM object written in managed code (C++/CLI). I am using that object in standard C++.
How do I force my COM object's destructor to be called immediately when the COM object is released? If that's not possible, call I have Release() call a MyDispose() method on my COM object?

My code to declare the object (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();
    };

My code to use the object (native 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().

If I put a destructor method on my COM object, it is never called. If I put a finalizer method, it is called when the garbage collector gets around to it. If I explicitly call my Release() override it is never called.

I would really like it so that when my native Bar::IFoo object goes out of scope it automatically calls my .NET object's dispose code. I would think I could do it by overriding the Release(), and if the object count = 0 then call MyDispose(). But apparently I'm not overriding Release() correctly because my Release() method is never called.

Obviously, I can make this happen by putting my MyDispose() method in the interface and requiring the people using my object to call MyDispose() before Release(), but it would be slicker if Release() just cleaned up the object.

Is it possible to force the .NET COM object's destructor, or some other method, to be called immediately when a COM object is released?

Googling on this issue gets me a lot of hits telling me to call System.Runtime.InteropServices.Marshal.ReleaseComObject(), but of course, that's how you tell .NET to release a COM object. I want COM Release() to Dispose of a .NET object.

like image 820
mhenry1384 Avatar asked Nov 12 '09 19:11

mhenry1384


2 Answers

I have a COM object written in managed code (C++/CLI). I am using that object in standard C++. How do I force my COM object's destructor to be called immediately when the COM object is released? If that's not possible, can I have Release() call a Dispose() (not MyDispose() - GBG) method on my (managed DotNet - GBG) COM object?

RE: Forcing a deterministic release of resources bound by a DotNet COM Server when the client is unmanaged code. These resource can be arranged to be released when the Garbage Collector collects the item, but that is not deterministic and such resources as file streams may not be released for hours or days for large memory systems where garbage collection is infrequent.

This is a common problem with COM Callable Wrappers (CCW's) as can be seen by another somewhat related thread at: Is it possible to intercept (or be aware of) COM Reference counting on CLR objects exposed to COM. in that case as in any case where one is writing their own COM client, no matter whether under managed or unmanaged code, is easy to solve just by a call to the IDisposable.Dispose() method as was done there. However, that method would not work for (say) a DotNet COM codec class whose client may be the operating system itself and which client shouldn't have to be aware that the COM server is unmanaged or managed (DotNet).

One can implement the IDisposable.Dispose() pattern on the DotNet COM server as per the MSDN link: http://msdn.microsoft.com/en-us/library/system.idisposable.aspx, but that won't do any good because the Dispose() method will never get called by the CCW. Ideally, the implementation of the CCW's in mscoree.dll should really check for and call the IDisposable.Dispose() method if implemented as part of the CCW release and/or finalization/destructor. I'm not sure why Microsoft did not do this, as having full access to the Assembly information they could easily determine if the DotNet COM Class supports IDisposable and just call Dispose() on final release if it does, and since this would be within the CCW, all the complexities about handling reference counting due to the extra interface reference could be avoided.

I can't see how this would "break" any existing code, as any client that is IDisposable aware could still call Dispose(), which if implemented according to the above template only effectively does anything on the first call. Microsoft may be worried about a class being Disposed while there are still managed references to it that wouldn't know about it being Disposed until there start to be exceptions thrown by trying to use already disposed resources, but that is a potential problem for any improper use of the IDisposable interface even with only DotNet clients: if there are multiple references to the same object instance and any one of them calls Dispose(), the others will find trying to use the required disposed resources cause exceptions. For such cases, one should always put in guards using the disposing boolean (as per the IDisposable pattern template) or only reference the object through a common wrapper.

Since Microsoft hasn't done the required few lines of code in the implementation of CCW's in mscoree.dll, I wrote a wrapper around mscoree.dll that adds this extra functionality. It is a little bit complex in that in order to control the creation of my wrapper around any instance of any DotNet COM Class, I need to also wrap the IClassFactory interface and aggregate the CCW instance in my "CCW_Wrapper" wrapper class. This wrapper also supports further levels of aggregation from yet another outer class. The code also does reference counting on instances of classes within the mscoree.dll implementation in use so as to be able to call FreeLibrary on mscoree.dll when there are no references (and LoadLibrary again if necessary later). The code should also be multi threading friendly as is required for COM under Windows 7. My C++ code is as follows:

EDITED 22 December 2010: Eliminated one unnecessary parameter of COM_Wrapper constructor:

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

A '.def' file is also required for the DLL as follows:

LIBRARY mscoreeCOM_DisposeWrapper

EXPORTS
    DllCanUnloadNow         PRIVATE
    DllGetClassObject       PRIVATE

To use this source code, compile it into a DLL and install the DLL into the Windows SYSTEM folder, then have your installation program or your [COMRegisterFunction] method in your DotNet COM server modify the class registry entry for the InprocServer32 from mscoree.dll to the name of this wrapper (say mscoreeWrapper.dll). It can be compiled under 32 and/or 64 bit, and for installation on 64-bit systems should have the 64-bit version put into the System folder and the 32-bit version put into the SysWOW64 folder; also, both the normal CLSID registration and the virtualized WOW6432 versions should be modified as to the InprocServer32 entries. Some applications may require this wrapper DLL to be digitally signed to work seamlessly, which is a whole other subject. If someone wants, I'll make a link to my compiled versions of these DLL's available here.

As I said, the required few lines (excluding the wrapper requirements) technique should really be incorporated into mscoree.dll. Does anyone know how to contact someone within the appropriate department within Microsoft to make this suggestion?

EDITADD: I have submitted a suggestion for the DotNet Framework to Microsoft Connect. This seems to be the best way of giving Microsoft feedback.

EDITADD2: In implementing a workaround for this problem, I realized why MIcrosoft likely will not implement this "Auto calling of Dispose, if supported, when the CCW reference count drops to zero". In writing the workaround, I had to obtain a reference pointer to a COM interface on a managed object in order to pass it to a pure unmanaged COM method, and then had to Release() that reference count in order to not have the CCW strongly referencing the object and therefore causing a memory leak by never having it available for garbage collection. I did this because I know that decreasing a reference count to zero on a managed object currently only makes the CCW drop its strong reference to the object, making it eligible for garbage collection if there are no other references. However, if Microsoft implemented the Auto Dispose fix as I have suggested or if this code was in place wrapping the mscoree.dll functionality, this would trigger a Dispose() on the managed object when it wasn't wanted. For this particular case, I could "guard" the Dispose(bool disposing) virtual method to prevent the Dispose() happening, but for any existing code that uses this behaviour with the same assumption, including Microsoft's implementation of the DotNet Runtime Libraries, implementing this "fix" on the CCW would break that existing code. This wrapper fix still applies for COM Servers one writes themselves and is aware of this side effect, as they can put the "guards" in place on the Dispose().

EDITADD 3: In further work with this, I see that my suggestion to Microsoft is still valid and the problems of "breaking" existing code can be avoided by the fix that would call the IDisposable.Dispose() method on the object instance implementing the managed COM server if the interface exists only if a new custom attribute such as [AutoComDispose(true)], whose default value is false, is applied to the managed COM server class. In this way, the programmer would choose to implement the functionality and the documentation on the new attribute would caution its use regarding having to "guard" the Dispose() method such as with a "artificial reference count" when there is a possibility of the Marshal.Release() method being called explicitly by in code used by the managed server or implicitly by calls to methods such as Marshal.GetObjectForIUnknown(), which in some cases can call QueryInterface and Release of the reference point if the ComObject is a managed object.

The main problem with this Answer is the complexity of installing it for use, as noted above.

like image 180
GordonBGood Avatar answered Oct 10 '22 03:10

GordonBGood


Actually neither the Dispose (or should i say ~Foo) not the Release will be called from the COM client when the last reference is released. It simply is not implemented. Here is some idea how such a thing could be done.

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

But the method is not advised even by author.

If you implement the COM Client as well the best option would be to query for IDisposable and call Dispose explicitly, iid to request is:

{805D7A98-D4AF-3F0F-967F-E5CF45312D2C}

Other option I can think of is to implement some sort of own "COM Garbage Collector". Each object created by COM would be placed in a list (Provided that objects of your type are only creatable by COM - I cannot think of any method to distinguish from where the object is created). And then you would have to periodically check the list, and on each object call something like this:

IntPtr iUnk = Marshal.GetIUnknownForObject(@object);
int refCount = Marshal.Release(iUnk);
if (refCount == 0)
    @object.Dispose();

but this is some crazy idea.

like image 38
kossib Avatar answered Oct 10 '22 03:10

kossib