20

我已经改写了这个问题。

当 .net 对象通过 COM iterop 暴露给 COM 客户端时,会创建一个 CCW(COM 可调用包装器),它位于 COM 客户端和托管 .net 对象之间。

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

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

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

那么,是否有可能知道 CLR 类的COM 可调用包装器的引用计数何时变为零?
和/或
是否可以在.net 中为 CCW 提供我的 AddRef 和 ReleaseRef 实现?

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

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

我不想做什么

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

谢谢
BW

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

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

谢谢大家。

4

10 回答 10

6

我意识到这是一个有点老的问题,但我确实得到了实际的工作请求。

它所做的是将已创建对象的 VTBL(s) 中的 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);
    }
}
于 2011-08-01T22:18:12.160 回答
5

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

我在名为 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-Script COM 类,该类公开了 .NET 数学库。这里我在Construction和Destruction期间在屏幕上弹出一些东西,注意在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 库中调用的对话框,最后在公开 .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.")
于 2010-02-12T14:30:17.290 回答
4

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

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

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

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

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

于 2010-02-14T03:30:53.047 回答
3

我也一直在为此苦苦挣扎,试图让我的预览处理程序的服务器生命周期正确,如下所述: 使用我们的托管预览处理程序框架以您的方式查看数据

我需要把它放到一个进程外的服务器中,突然间我遇到了生命周期控制问题。

此处为感兴趣的人描述了进入进程外服务器的方法: RegistrationSrvices.RegisterTypeForComClients 社区内容 ,这意味着您可以通过实施 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; 
}
于 2011-05-23T20:35:18.100 回答
2

.Net 框架的工作方式不同,请参阅:
.NET 框架提供的内存管理技术与基于 COM 的世界中内存管理的工作方式不同。COM 中的内存管理是通过引用计数来实现的。.NET 提供了一种涉及引用跟踪的自动内存管理技术。在本文中,我们将了解公共语言运行时 CLR 使用的垃圾收集技术。

无计可施

[已编辑] 再来一轮...

看看这个替代方法Importing a Type Library as an Assembly
正如您自己所说的使用CCW,您可以以传统的 COM 方式访问引用计数

[已编辑] 坚持是一种美德
你知道WinAPIOverride32吗?有了它,您可以捕捉并研究它是如何工作的。另一个可以提供帮助的工具是Deviare COM Spy Console
这并不容易。
祝你好运。

于 2010-02-12T16:40:34.800 回答
2

据我所知,关于这个主题的最佳报道是在 Alan Gordon 的The .NET and COM Interoperability Handbook一书中,该链接应该转到 Google Books 中的相关页面。(不幸的是我没有它,我去买了Troelsen 的书。)

那里的指导暗示没有一种明确定义的方式来挂钩ReleaseCCW 中的 /reference 计数。相反,建议您将 C# 类设为一次性,并鼓励您的 COM 客户端(在您的情况下为 VBScript 作者)Dispose在需要确定性完成时调用。

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

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

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

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

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

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

因此,现在您的 VBScript 客户端正在通过 CCW 与 C# 类对话,但您可以截获最终版本并将其转发给Dispose方法。

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

于 2010-02-15T12:40:54.047 回答
0

我想这不可能的原因是引用计数为 0 并不意味着该对象未在使用中,因为您可能有一个调用图,例如

VB_Object
   |
   V
   |
Managed1 -<- Managed2

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

如果你真的需要按照你说的做,我想你可以在非托管 C++ 中创建包装类,当引用计数下降到 0 时调用 Dispose 方法。这些类可能是从元数据中代码生成的,但我没有任何经验在如何实现这种事情。

于 2010-02-09T11:48:14.757 回答
0

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

于 2010-02-16T16:46:18.920 回答
-1

为什么不转变范式。如何围绕暴露创建自己的聚合并使用通知方法扩展。它甚至可以在 .Net 中完成,而不仅仅是 ATL。

已编辑:这是一些可能以另一种方式描述的链接(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:采取那里(http://vb.mvps.org/hardcore/html/countingreferences.htm)最简单的 IUnknown 与发送事件

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
于 2010-02-08T16:49:26.577 回答
-1

据我所知,GC 已经为您尝试做的事情提供了支持。它被称为终结。在纯托管的世界中,最佳实践是避免 Finalization,因为它有一些副作用,会对 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 执行而不冻结应用程序线程而不是更改其基本操作。)

于 2010-02-14T04:33:10.467 回答