Search code examples
c#.netcom-interopreference-counting

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


Solution

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