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

前端 未结 10 1036
礼貌的吻别
礼貌的吻别 2020-12-08 02:56

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

相关标签:
10条回答
  • 2020-12-08 03:34

    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);
        }
    }
    
    0 讨论(0)
  • 2020-12-08 03:47

    I guess the reason for this not being possible is that a refcount of 0 does not mean that the object is not in use, because you might have a call graph like

    VB_Object
       |
       V
       |
    Managed1 -<- Managed2
    

    in which case the object Managed1 is still in use even if the VB object drops its reference to it and its refcount therefore is 0.

    If you really need to do what you say, I guess you could create wrapper classes in unmanaged C++, which invokes the Dispose method when the refcount drops to 0. These classes could probably be codegen'd from metadata, but I have no experience whatsoever in how to do implement this kind of thing.

    0 讨论(0)
  • 2020-12-08 03:49

    OK Folks, here's another attempt at it. You can actually use "Windows Script Components" to wrap your .NET COM objects and get finalization that way. Here's a full sample using a simple .NET Calculator which can Add values. I'm sure you'll get the concept from there, this totally avoids the VB-Runtime, ATL issues and uses the Windows Scripting Host which is available on every major WIN32/WIN64 platform.

    I created a simple COM .NET Class called Calculator in a namespaces called DemoLib. Note this implements IDisposable where for demo purpose I put something up on the screen to show it has terminated. I'm sticking totally to vb here in .NET and script to keep things simple, but the .NET portion can be in C# etc. When you save this file you'll need to register it with regsvr32, it will need to be saved as something like CalculatorLib.wsc.

    <ComClass(Calculator.ClassId, Calculator.InterfaceId, Calculator.EventsId)> _
    Public Class Calculator
        Implements IDisposable
    #Region "COM GUIDs"
        ' These  GUIDs provide the COM identity for this class 
        ' and its COM interfaces. If you change them, existing 
        ' clients will no longer be able to access the class.
        Public Const ClassId As String = "68b420b3-3aa2-404a-a2d5-fa7497ad0ebc"
        Public Const InterfaceId As String = "0da9ab1a-176f-49c4-9334-286a3ad54353"
        Public Const EventsId As String = "ce93112f-d45e-41ba-86a0-c7d5a915a2c9"
    #End Region
        ' A creatable COM class must have a Public Sub New() 
        ' with no parameters, otherwise, the class will not be 
        ' registered in the COM registry and cannot be created 
        ' via CreateObject.
        Public Sub New()
            MyBase.New()
        End Sub
        Public Function Add(ByVal x As Double, ByVal y As Double) As Double
            Return x + y
        End Function
        Private disposedValue As Boolean = False        ' To detect redundant calls
        ' IDisposable
        Protected Overridable Sub Dispose(ByVal disposing As Boolean)
            If Not Me.disposedValue Then
                If disposing Then
                    MsgBox("Disposed called on .NET COM Calculator.")
                End If
            End If
            Me.disposedValue = True
        End Sub
    #Region " IDisposable Support "
        ' This code added by Visual Basic to correctly implement the disposable pattern.
        Public Sub Dispose() Implements IDisposable.Dispose
            ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub
    #End Region
    End Class
    

    Next I create A Windows Script Component called Calculator.Lib which has a single method which returns back a VB-Script COM class which exposes the .NET Math Library. Here I pop up something on the screen during Construction and Destruction, note in the Destruction we call the Dispose method in the .NET library to free up resources there. Note the use of the Lib() function to return the .NET Com Calculator to the caller.

    <?xml version="1.0"?>
    <component>
    <?component error="true" debug="true"?>
    <registration
        description="Demo Math Library Script"
        progid="Calculator.Lib"
        version="1.00"
        classid="{0df54960-4639-496a-a5dd-a9abf1154772}"
    >
    </registration>
    <public>
      <method name="GetMathLibrary">
      </method>
    </public>
    <script language="VBScript">
    <![CDATA[
    Option Explicit
    '-----------------------------------------------------------------------------------------------------
    ' public Function to return back a logger.
    '-----------------------------------------------------------------------------------------------------
    function GetMathLibrary()
        Set GetMathLibrary = New MathLibrary
    end function
    Class MathLibrary
        private dotNetMatFunctionLib
      private sub class_initialize()
        MsgBox "Created."
        Set dotNetMatFunctionLib = CreateObject("DemoLib.Calculator")
      end sub
      private sub class_terminate()
            dotNetMatFunctionLib.Dispose()
            Set dotNetMatFunctionLib = nothing
        MsgBox "Terminated."
      end sub
      public function Lib()
        Set Lib = dotNetMatFunctionLib
      End function
    end class
    ]]>
    </script>
    </component>
    

    Finally to tie it all together here's s sample VB script where you get dialogues showing creation, the calculation, dispose being called in the .NET library and finally Terminate in the COM component exposing the .NET Component.

    dim comWrapper
    dim vbsCalculator
    set comWrapper = CreateObject("Calculator.Lib")
    set vbsCalculator = comWrapper.GetMathLibrary()
    msgbox "10 + 10 = " & vbsCalculator.lib.Add(10, 10)
    msgbox "20 + 20 = " & vbsCalculator.lib.Add(20, 20)
    set vbsCalculator = nothing
    MsgBox("Dispose & Terminate should have been called before here.")
    
    0 讨论(0)
  • 2020-12-08 03:49

    I haven't verified this, but here is what I would try:

    First, here is a CBrumme Blog article about the clr's default implementation of IMarshal. If your utilities are used across COM apartments you won't get proper com behavior from a direct port of VB6 to the CLR. Com objects implemented by the CLR act as if they aggregated the free threaded marshaller rather than apartment threaded model that VB6 exposed.

    You can implement IMarshal (on the clr class you are exposing as a com object). My understanding is that will allow you control over creating the COM proxy (not the interop proxy). I think this will allow you to trap the Release calls in the object you returned from UnmarshalInterface and signal back to your original object. I'd wrap the standard marshaller (e.g. pinvoke CoGetStandardMarshaler) and forward all calls to it. I believe that object will have a lifetime tied to the lifetime of the the CCW.

    again ... this is what I'd try if I had to solve it in C#.

    On the other hand, would this kind of solution really be easier than implementing in ATL? Just because the magic part is written in C# doesn't make the solution simple. If what I propose above does solve the problem, you'll need to write a really big comment explaining what was going on.

    0 讨论(0)
  • 2020-12-08 03:50

    I've been struggling with this too, to try to get server lifetime correct for my Preview Handler, as described here: View Data Your Way With Our Managed Preview Handler Framework

    I needed to get it into an out of process server, and suddenly I had lifetime control issues.

    The way to get into an out of process server is described here for anyone interested: RegistrationSrvices.RegisterTypeForComClients community content which implies that you may be able to do it by implmenting IDispose, but that didn't work.

    I tried implementing a finalizer, which did eventually cause the object to be released, but because of the pattern of usage of the server calling my object, it meant my server hung around forever. I also tried spinning off a work item, and after a sleep, forcing a garbage collect, but that was really messy.

    Instead, it came down to hooking Release (and AddRef because the return value of Release couldn't be trusted).

    (Found via this post: http://blogs.msdn.com/b/oldnewthing/archive/2007/04/24/2252261.aspx#2269675)

    Here's what I did in my object's constructor:

    //  Get the CCW for the object
    _myUnknown = Marshal.GetIUnknownForObject(this);
    IntPtr _vtable = Marshal.ReadIntPtr(_myUnknown);
    
    // read out the AddRef/Release implementation
    _CCWAddRef = (OverrideAddRef)
        Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(_vtable, 1 * IntPtr.Size), typeof(OverrideAddRef));
    
    _CCWRelease = (OverrideRelease)
        Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(_vtable, 2 * IntPtr.Size), typeof(OverrideRelease)); 
    _MyRelease = new OverrideRelease(NewRelease);
    _MyAddRef = new OverrideAddRef(NewAddRef);
    
    
    Marshal.WriteIntPtr(_vtable, 1 * IntPtr.Size, Marshal.GetFunctionPointerForDelegate(_MyAddRef)); 
    Marshal.WriteIntPtr(_vtable, 2 * IntPtr.Size, Marshal.GetFunctionPointerForDelegate(_MyRelease));
    
    and the declarations:
    
    
    int _refCount; 
    
    delegate int OverrideAddRef(IntPtr pUnknown);
    OverrideAddRef _CCWAddRef; 
    OverrideAddRef _MyAddRef;
    
    
    delegate int OverrideRelease(IntPtr pUnknown); 
    OverrideRelease _CCWRelease;
    OverrideRelease _MyRelease;
    
    IntPtr _myUnknown;
    
    protected int NewAddRef(IntPtr pUnknown) 
    {
        Interlocked.Increment(ref _refCount);
        return _CCWAddRef(pUnknown); 
    }
    
    
    protected int NewRelease(IntPtr pUnknown) 
    {
        int ret = _CCWRelease(pUnknown);
    
        if (Interlocked.Decrement(ref _refCount) == 0)
        {
            ret = _CCWRelease(pUnknown);
            ComServer.Unlock();
        }
    
        return ret; 
    }
    
    0 讨论(0)
  • 2020-12-08 03:51

    From the .NET, request an IUnknown on the object. Call AddRef(), then Release(). Then take the return value of AddRef() and run with it.

    0 讨论(0)
提交回复
热议问题