开发者

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

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 fo开发者_JAVA百科r 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.


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


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.")


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.


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


.Net framework works differently, see:
The .NET Framework provides memory management techniques that differ from the way memory management worked in a COM-based world. The memory management in COM was through reference counting. .NET provides an automatic memory management technique that involves reference tracing. In this article, we'll take a look at the garbage collection technique used by the Common Language Runtime CLR.

nothing to be done

[EDITED] more one round...

Take a look at this alternative Importing a Type Library as an Assembly
As you yourself said using CCW you can access reference-counte in traditional COM fashion.

[EDITED] Persistence is a virtue
You know WinAPIOverride32? With it you can capture and study how it works. Another tool that can help is Deviare COM Spy Console.
This will not be easy.
Good luck.


As far as I'm aware, the best coverage of this subject is in the book The .NET and COM Interoperability Handbook By Alan Gordon, and that link should go to the relevant page in Google Books. (Unfortunately I don't have it, I went for the Troelsen book instead.)

The guidance there implies that there isn't a well-defined way of hooking into the Release/reference counting in the CCW. Instead the suggestion is that you make your C# class disposable, and encourage your COM clients (in your case the VBScript authors) to call Dispose when they want deterministic finalisation to occur.

But happily there is a loophole for you because your clients are late-binding COM clients, because VBScript uses IDispatch to make all calls to objects.

Suppose your C# classes were exposed via COM. Get that working first.

Now in ATL/C++ create a wrapper class, using the ATL Simple Object wizard, and in the options page choose Interface: Custom instead of Dual. This stops the wizard putting in its own IDispatch support.

In the class's constructor, use CoCreateInstance to magic up an instance of your C# class. Query it for IDispatch and hold onto that pointer in a member.

Add IDispatch to the wrapper class's inheritance list, and forward all four methods of IDispatch straight through to the pointer you stashed away in the constructor.

In the FinalRelease of the wrapper, use the late binding technique (Invoke) to call the Dispose method of the C# object, as described in the Alan Gordon book (on the pages I linked to above).

So now your VBScript clients are talking via the CCW to the C# class, but you get to intercept the final release and forward it to the Dispose method.

Make your ATL library expose a separate wrapper for each "real" C# class. You'll probably want to use inheritance or templates to get good code reuse here. Each C# class you support should only require a couple of lines in the ATL wrapping code.


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.


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


Why don't shift paradigm. What about to create your own aggregate around exposed and extend with notification methods. It even can be done in .Net not only by ATL.

EDITED: Here is some link that may be describe some another way(http://msdn.microsoft.com/en-us/library/aa719740(VS.71).aspx). But following steps explains my idea above.

Create create new .Net class that implements your legacy interface(ILegacy), and new interface (ISendNotify) with single method:

interface ISendNotify 
{
     void SetOnDestroy(IMyListener );
}

class MyWrapper : ILegacy, ISendNotify, IDisposable{ ...

Inside the MyClass create instance of your real legacy object, and delegate all calls from MyClass to this instance. This is an aggregation. So lifetime of aggregate now depends on MyClass. Since MyClass is a IDisposable now you can intercept when instance is deleted, so you can send notification by IMyListener

EDIT2: Taken there (http://vb.mvps.org/hardcore/html/countingreferences.htm) simplest impl of IUnknown with sending event

Class MyRewritten
    ...
    Implements IUnknown
    Implements ILegacy
    ...
    Sub IUnknown_AddRef()
        c = c + 1
    End Sub

    Sub IUnknown_Release()
        c = c - 1
        If c = 0 Then
            RaiseEvent Me.Class_Terminate
            Erase Me
        End If
    End Sub


To my knowledge, the GC already provides support for what you are trying to do. It is called Finalization. In a purely managed world, best practice is to avoid Finalization, as it has some side effects that can negatively impact the performance and operation of the GC. The IDisposable interface provides a clean, managed way of bypassing object finalization and providing cleanup of both managed and unmanaged resources from within managed code.

In your case, you need to initiate cleanup of a managed resource once all unmanaged references have been released. Finalization should excel at solving your problem here. The GC will finalize an object, always, if a finalizer is present, regardless of how the last references to a finalizable object were released. If you implement a finalizer on your .NET type (just implement a destructor), then the GC will place it in the finalization queue. Once the GC collection cycle is complete, it will process the finalization queue. Any cleanup work you perform in your destructor will occur once the finalization queue is processed.

It should be noted that if your finalizable .NET type contains references to other .NET objects which in turn require finalization, you could invoke a lengthy GC collection, or some of the objects may survive for longer than they would without finalization (which would mean they survive a collection and reach the next generation, which is collected less frequently.) However, if the cleanup work of your .NET objects that use CCW's is not time sensitive in any fashion, and memory usage is not a huge issue, some extra lifetime shouldn't matter. It should be noted that finalizable objects should be created with care, and minimizing or eliminating any class instance level references to other objects can improve your overall memory management via the GC.

You can read more about finalization in this article: http://msdn.microsoft.com/en-us/magazine/bb985010.aspx. While it is a rather old article from back when .NET 1.0 was first released, the fundamental architecture of the GC is unchanged as of yet (the first significant changes to the GC will be arriving with .NET 4.0, however they are related more to concurrent GC execution without freezing the application threads than changes to its fundamental operation.)

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜