是否可以拦截(或意识到)COM引用计数暴露给COM的CLR对象

时间:2010-02-08 16:38:47

标签: c# .net com-interop reference-counting

我已经改写了这个问题。

当.net对象通过COM iterop公开给COM客户端时,会创建一个CCW(COM Callable Wrapper),它位于COM客户端和托管.net对象之间。

在COM世界中,对象会记录其他对象对其的引用数。当引用计数变为零时,将删除/释放/收集对象。这意味着COM对象终止是确定性的(我们在.net中使用Using / IDispose进行确定性终止,对象终结器是非确定性的)。

每个CCW都是一个COM对象,它的引用计数与任何其他COM对象一样。当CCW死亡(引用计数变为零)时,GC将无法找到CCW包装的CLR对象,并且CLR对象有资格进行收集。快乐的日子,一切都与世隔绝。

我想要做的是在CCW死时(即当它的引用计数变为零时)捕获,并以某种方式将此信号通知给CLR对象(例如,通过在托管对象上调用Dispose方法)。

那么,是否可以知道CLR类的COM Callable Wrapper的引用计数何时变为零?
和/或
是否有可能提供我的AddRef& amp;针对.net中的CCW的ReleaseRef

如果不是替代方法是在ATL中实现这些DLL(我不需要任何ATL帮助,谢谢)。它不是火箭科学,但我不愿意这样做,因为我是内部唯一的开发人员,任何现实世界的C ++或任何ATL。

背景
我在.net中重写了一些旧的VB6 ActiveX DLL(确切地说是C#,但这更像是.net / COM互操作问题,而不是C#问题)。一些旧的VB6对象依赖于引用计数来在对象终止时执行操作(参见上面引用计数的解释)。这些DLL不包含重要的业务逻辑,它们是我们为使用VBScript与我们集成的客户提供的实用程序和帮助程序函数。

我不想做什么

  • 引用计数.net对象 使用垃圾收集器。 我很满意GC,我的 问题不在于GC。
  • 使用对象终结器。终结者是 非确定性的,在这种情况下我 需要确定性终止(如 .net中的使用/ IDispose惯用语
  • 在非托管C ++中实现IUnknown
    如果我要去C ++路线,我将使用 ATL,谢谢。
  • 使用Vb6解决此问题,或重新使用 VB6对象。整点 这个练习是删除我们的构建 对Vb6的依赖。

感谢
BW

接受的答案
感谢Steve Steiner感谢Earwicker,他提出了唯一的(可能可行的)基于.net的答案,而Bigtoe提出了一个非常简单的ATL解决方案。

然而,接受的答案是{{3}},他建议将.net对象包装在VbScript对象中(我认为不诚实),有效地为VbScript问题提供了简单的VbScript解决方案。

感谢所有人。

10 个答案:

答案 0 :(得分:5)

好的伙计,这是另一次尝试。实际上,您可以使用“Windows脚本组件”来包装.NET COM对象并以此方式完成最终化。这是一个使用简单的.NET计算器的完整示例,它可以添加值。我相信你会从那里得到这个概念,这完全避免了VB-Runtime,ATL问题,并使用了Windows Scripting Host,它可以在每个主要的WIN32 / WIN64平台上使用。

我在名为DemoLib的名称空间中创建了一个名为Calculator的简单COM .NET类。请注意,这实现了IDisposable,为了演示的目的,我在屏幕上放了一些东西以显示它已经终止。我在.NET和脚本中完全坚持vb以保持简单,但.NET部分可以在C#等。当你保存这个文件时你需要用regsvr32注册它,它需要保存就像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

接下来,我创建了一个名为Calculator.Lib的Windows脚本组件,该组件有一个返回VB脚本COM类的方法,该类公开了.NET Math Library。在构建和销毁期间,我在屏幕上弹出一些内容,请注意在Destruction中我们在.NET库中调用Dispose方法来释放资源。请注意使用Lib()函数将.NET Com Calculator返回给调用者。

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

最后,将这一切联系在一起的示例VB脚本中,您将获得对话框,显示创建,计算,在.NET库中调用dispose,最后在暴露.NET组件的COM组件中终止。

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

答案 1 :(得分:5)

我意识到这是一个有点老问题,但我确实得到了一些时间回来的实际请求。

它的作用是使用自定义实现替换创建对象的VTBL中的Release,该实现在释放所有引用时调用Dispose。请注意,无法保证始终有效。主要假设是标准CCW的所有接口上的所有Release方法都是相同的方法。

使用风险自负。 :)

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

答案 2 :(得分:4)

我还没有证实这一点,但这是我会尝试的:

首先,这是关于clr默认的IMarshal实现的CBrumme Blog article。如果您的实用程序在COM公寓中使用,则无法从VB6的直接端口到CLR获得正确的com行为。 CLR实现的Com对象就好像它们聚合了自由线程编组器而不是VB6公开的单元线程模型。

您可以实现IMarshal(在您作为com对象公开的clr类上)。我的理解是,允许您控制创建COM代理(而不是互操作代理)。我认为这将允许您在从UnmarshalInterface返回的对象中捕获Release调用,并将信号发送回原始对象。我将包装标准编组器(例如,pinvoke CoGetStandardMarshaler)并将所有调用转发给它。我相信该对象的生命周期与CCW的生命周期有关。

再次......如果我必须在C#中解决它,这就是我要尝试的。

另一方面,这种解决方案真的比在ATL中实现更容易吗?仅仅因为魔术部分是用C#编写的,并不能使解决方案变得简单。如果我上面提出的建议解决了这个问题,那么你需要写一个非常重要的评论来解释发生了什么。

答案 3 :(得分:3)

我一直在努力解决这个问题,试图让我的预览处理程序的服务器生命周期正确,如下所述: View Data Your Way With Our Managed Preview Handler Framework

我需要让它进入一个进程外服务器,突然间我遇到了终身控制问题。

这里描述了进入流程外服务器的方式,对于任何感兴趣的人: RegistrationSrvices.RegisterTypeForComClients community content 这意味着您可以通过实施IDispose来实现,但这不起作用。

我尝试实现一个终结器,最终导致该对象被释放,但由于调用我的对象的服务器的使用模式,这意味着我的服务器永远存在。我也试过脱掉工作项目,睡了之后,强行收集垃圾,但那真的很乱。

相反,它归结为挂钩Release(和AddRef,因为Release的返回值不可信)。

(通过这篇文章找到:http://blogs.msdn.com/b/oldnewthing/archive/2007/04/24/2252261.aspx#2269675

这是我在对象的构造函数中所做的:

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

答案 4 :(得分:2)

答案 5 :(得分:2)

据我所知,此主题的最佳报道在书中The .NET and COM Interoperability Handbook由Alan Gordon撰写,该链接应转到Google图书的相关页面。 (不幸的是我没有它,而是选择了Troelsen book。)

那里的指导意味着没有一种定义明确的方式来加入CCW中的Release /引用计数。相反,建议是让你的C#类是一次性的,并鼓励你的COM客户端(在你的情况下是VBScript作者)在他们想要确定性的最终化发生时调用Dispose

但很高兴你有一个漏洞,因为你的客户端是后期绑定的COM客户端,因为VBScript使用IDispatch来对对象进行所有调用。

假设您的C#类是通过COM公开的。让它先工作。

现在在ATL / C ++中使用ATL Simple Object向导创建一个包装类,并在选项页面中选择Interface:Custom而不是Dual。这会阻止向导放入自己的IDispatch支持。

在类的构造函数中,使用CoCreateInstance来表示C#类的实例。查询IDispatch并在成员中保留该指针。

IDispatch添加到包装类的继承列表中,并将IDispatch的所有四种方法直接转发到您在构造函数中隐藏的指针。

在包装器的FinalRelease中,使用后期绑定技术(Invoke)来调用C#对象的Dispose方法,如Alan Gordon书中所述(在我上面链接的页面。)

现在,您的VBScript客户端正在通过CCW与C#类进行通信,但您可以截取最终版本并将其转发到Dispose方法。

让您的ATL库为每个“真正的”C#类公开一个单独的包装器。您可能希望使用继承或模板来获得良好的代码重用。您支持的每个C#类在ATL包装代码中只需要几行。

答案 6 :(得分:0)

我猜这不可能的原因是引用计数0并不意味着该对象未被使用,因为您可能有一个像

这样的调用图
VB_Object
   |
   V
   |
Managed1 -<- Managed2

在这种情况下,对象Managed1仍在使用中,即使VB对象删除了对它的引用,因此其引用计数为0。

如果你真的需要做你说的话,我想你可以在非托管C ++中创建包装类,当refcount降为0时调用Dispose方法。这些类可能是从元数据中编码的,但我有没有任何关于如何实现这种事情的经验。

答案 7 :(得分:0)

从.NET请求对象上的IUnknown。调用AddRef(),然后调用Release()。然后获取AddRef()的返回值并使用它运行。

答案 8 :(得分:-1)

为什么不转换范式。如何使用通知方法围绕公开和扩展创建自己的聚合。甚至可以通过ATL在.Net中完成。

<强> EDITED : 这是一些可能以另一种方式描述的链接(http://msdn.microsoft.com/en-us/library/aa719740(VS.71).aspx)。但是以下步骤解释了我的想法。

使用单一方法创建实现旧版界面(ILegacy)和新界面(ISendNotify)的新.Net类:

interface ISendNotify 
{
     void SetOnDestroy(IMyListener );
}

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

在MyClass中创建真实遗留对象的实例,并将MyClass中的所有调用委托给此实例。这是一个聚合。所以聚合的生命周期现在取决于MyClass。由于MyClass现在是IDisposable,您可以在删除实例时拦截,因此您可以通过IMyListener发送通知

EDIT2 :带有发送事件的IUnknown(http://vb.mvps.org/hardcore/html/countingreferences.htm)最简单的impl

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

答案 9 :(得分:-1)

据我所知,GC已经为你要做的事情提供了支持。它被称为终结。在纯粹管理的世界中,最佳做法是避免终结,因为它有一些副作用可能会对GC的性能和操作产生负面影响。 IDisposable接口提供了一种干净,可管理的绕过对象最终化的方法,并提供了托管代码中托管和非托管资源的清理。

在您的情况下,您需要在释放所有非托管引用后启动托管资源的清理。最终确定应该擅长解决您的问题。如果存在终结器,GC将始终最终确定一个对象,而不管最终对象的最后一次引用是如何释放的。如果在.NET类型上实现终结器(只实现析构函数),则GC会将其置于终结队列中。 GC收集周期完成后,它将处理完成队列。处理完最终化队列后,将在析构函数中执行任何清理工作。

应该注意的是,如果最终的.NET类型包含对其他.NET对象的引用,而这些对象又需要最终化,那么您可以调用一个冗长的GC集合,或者某些对象可能存活的时间比没有完成时更长(这意味着它们可以在一个集合中存活并到达下一代,而这种集合的收集频率较低。)但是,如果使用CCW的.NET对象的清理工作在任何方面都不是时间敏感的,并且内存使用不是一个大问题一些额外的寿命应该无关紧要。应该注意的是,应该小心创建可终结的对象,并且最小化或消除对其他对象的任何类实例级引用可以通过GC改善整体内存管理。

您可以在本文中详细了解最终结果:http://msdn.microsoft.com/en-us/magazine/bb985010.aspx。虽然它是.NET 1.0首次发布时的一篇相当古老的文章,但GC的基本架构尚未改变(GC的第一次重大改变将随.NET 4.0推出,但它们更多地与并发GC执行而不冻结应用程序线程而不是更改其基本操作。)