C# – Is it possible to intercept (or be aware of) COM Reference counting on CLR objects exposed to COM

ccom-interopnetreference-counting

I have rephrased this question.

When .net objects are exposed to COM Clients through COM iterop, a CCW (COM Callable Wrapper) is created, this sits between the COM Client and the Managed .net object.

In the COM world, objects keep a count of the number of references that other objects have to it. Objects are deleted/freed/collected when that reference count goes to Zero. This means that COM Object termination is deterministic (we use Using/IDispose in .net for deterministic termination, object finalizers are non deterministic).

Each CCW is a COM object, and it is reference counted like any other COM object. When the CCW dies (reference count goes to Zero) the GC won't be able to find the CLR object the CCW wrapped, and the CLR object is eligible for collection. Happy days, all is well with the world.

What I would like to do is catch when the CCW dies (i.e. when its reference count goes to zero), and somehow signal this to the CLR object (e.g. By calling a Dispose method on the managed object).

So, is it possible to know when the reference count of a COM Callable Wrapper for a CLR class goes to Zero?
and/or
Is it possible to provide my implementation of AddRef & ReleaseRef for CCWs in .net?

If not the alternative is to implement these DLLs in ATL (I don't need any help with ATL, thanks). It wouldn't be rocket science but I'm reluctant to do it as I'm the only developer in-house with any real world C++, or any ATL.

Background
I'm re-writing some old VB6 ActiveX DLLs in .net (C# to be exact, but this is more a .net / COM interop problem rather than a C# problem). Some of the old VB6 objects depend on reference counting to carry out actions when the object terminates (see explaination of reference counting above). These DLL's don't contain important business logic, they are utilities and helper functions that we provide to clients that integrate with us using VBScript.

What I'm not trying to do

  • Reference count .net objects instead
    of the using the Garbage Collector.
    I'm quite happy with the GC, my
    problem isn't with the GC.
  • Use object finalizers. Finalizers are
    non deterministic, in this instance I
    need deterministic termination (like
    the Using/IDispose idiom in .net)
  • Implement IUnknown in unmanaged C++
    If I've to go the C++ route I'll use
    ATL, thanks.
  • Solve this using Vb6, or re-using the
    VB6 objects. The entire point of
    this exercise is to remove our build
    dependence on Vb6.

Thanks
BW

The Accepted Answer
Folks a thousand thanks to Steve Steiner, who came up with the only (possibly workable) .net based answer, and Earwicker, who came up with a very simple ATL solution.

However the accepted answer goes to Bigtoe, who suggests wrapping the .net objects in VbScript objects (which I hadn't considered to be honest), effectively providing a simple VbScript solution to a VbScript problem.

Thanks to all.

Best Answer

I realize this is somewhat old question, but I did get the actual request to work some time back.

What it does is replace Release in the VTBL(s) of the created object with a custom implementation that calls Dispose when all references have been released. Note that there are no guarantees to this will always work. The main assumption is that all Release methods on all interfaces of the standard CCW are the same method.

Use at your own risk. :)

/// <summary>
/// I base class to provide a mechanism where <see cref="IDisposable.Dispose"/>
/// will be called when the last reference count is released.
/// 
/// </summary>
public abstract class DisposableComObject: IDisposable
{
    #region Release Handler, ugly, do not look

    //You were warned.


    //This code is to enable us to call IDisposable.Dispose when the last ref count is released.
    //It relies on one things being true:
    // 1. That all COM Callable Wrappers use the same implementation of IUnknown.


    //What Release() looks like with an explit "this".
    private delegate int ReleaseDelegate(IntPtr unk);

    //GetFunctionPointerForDelegate does NOT prevent GC ofthe Delegate object, so we'll keep a reference to it so it's not GC'd.
    //That would be "bad".
    private static ReleaseDelegate myRelease = new ReleaseDelegate(Release);
    //This is the actual address of the Release function, so it can be called by unmanaged code.
    private static IntPtr myReleaseAddress = Marshal.GetFunctionPointerForDelegate(myRelease);


    //Get a Delegate that references IUnknown.Release in the CCW.
    //This is where we assume that all CCWs use the same IUnknown (or at least the same Release), since
    //we're getting the address of the Release method for a basic object.
    private static ReleaseDelegate unkRelease = GetUnkRelease();
    private static ReleaseDelegate GetUnkRelease()
    {
        object test = new object();
        IntPtr unk = Marshal.GetIUnknownForObject(test);
        try
        {
            IntPtr vtbl = Marshal.ReadIntPtr(unk);
            IntPtr releaseAddress = Marshal.ReadIntPtr(vtbl, 2 * IntPtr.Size);
            return (ReleaseDelegate)Marshal.GetDelegateForFunctionPointer(releaseAddress, typeof(ReleaseDelegate));
        }
        finally
        {
            Marshal.Release(unk);
        }
    }

    //Given an interface pointer, this will replace the address of Release in the vtable
    //with our own. Yes, I know.
    private static void HookReleaseForPtr(IntPtr ptr)
    {
        IntPtr vtbl = Marshal.ReadIntPtr(ptr);
        IntPtr releaseAddress = Marshal.ReadIntPtr(vtbl, 2 * IntPtr.Size);
        Marshal.WriteIntPtr(vtbl, 2 * IntPtr.Size, myReleaseAddress);
    }

    //Go and replace the address of CCW Release with the address of our Release
    //in all the COM visible vtables.
    private static void AddDisposeHandler(object o)
    {
        //Only bother if it is actually useful to hook Release to call Dispose
        if (Marshal.IsTypeVisibleFromCom(o.GetType()) && o is IDisposable)
        {
            //IUnknown has its very own vtable.
            IntPtr comInterface = Marshal.GetIUnknownForObject(o);
            try
            {
                HookReleaseForPtr(comInterface);
            }
            finally
            {
                Marshal.Release(comInterface);
            }
            //Walk the COM-Visible interfaces implemented
            //Note that while these have their own vtables, the function address of Release
            //is the same. At least in all observed cases it's the same, a check could be added here to
            //make sure the function pointer we're replacing is the one we read from GetIUnknownForObject(object)
            //during initialization
            foreach (Type intf in o.GetType().GetInterfaces())
            {
                if (Marshal.IsTypeVisibleFromCom(intf))
                {
                    comInterface = Marshal.GetComInterfaceForObject(o, intf);
                    try
                    {
                        HookReleaseForPtr(comInterface);
                    }
                    finally
                    {
                        Marshal.Release(comInterface);
                    }
                }
            }
        }
    }

    //Our own release. We will call the CCW Release, and then if our refCount hits 0 we will call Dispose.
    //Note that is really a method int IUnknown.Release. Our first parameter is our this pointer.
    private static int Release(IntPtr unk)
    {
        int refCount = unkRelease(unk);
        if (refCount == 0)
        {
            //This is us, so we know the interface is implemented
            ((IDisposable)Marshal.GetObjectForIUnknown(unk)).Dispose();
        }
        return refCount;
    }
    #endregion

    /// <summary>
    /// Creates a new <see cref="DisposableComObject"/>
    /// </summary>
    protected DisposableComObject()
    {
        AddDisposeHandler(this);
    }

    /// <summary>
    /// Calls <see cref="Dispose"/> with false.
    /// </summary>
    ~DisposableComObject()
    {
        Dispose(false);
    }

    /// <summary>
    /// Override to dispose the object, called when ref count hits or during GC.
    /// </summary>
    /// <param name="disposing"><b>true</b> if called because of a 0 refcount</param>
    protected virtual void Dispose(bool disposing)
    {

    }

    void IDisposable.Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}
Related Topic