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

我已经改写了这个问题。

当.net对象通过COM iterop公开给COM客户端时,会创建一个CCW( COM Callable Wrapper ),它位于COM客户端和Managed .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

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

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

谢谢大家。

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

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

 _ 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 Math Library。 在构建和销毁期间,我在屏幕上弹出一些内容,请注意在Destruction中我们在.NET库中调用Dispose方法来释放资源。 请注意使用Lib()函数将.NET Com Calculator返回给调用者。

            

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

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

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

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

使用风险由您自己承担。 🙂

 ///  /// I base class to provide a mechanism where  /// will be called when the last reference count is released. /// ///  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 ///  /// Creates a new  ///  protected DisposableComObject() { AddDisposeHandler(this); } ///  /// Calls  with false. ///  ~DisposableComObject() { Dispose(false); } ///  /// Override to dispose the object, called when ref count hits or during GC. ///  /// true if called because of a 0 refcount protected virtual void Dispose(bool disposing) { } void IDisposable.Dispose() { Dispose(true); GC.SuppressFinalize(this); } } 

我没有validation这一点,但这是我会尝试的:

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

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

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

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

我一直在努力解决这个问题,试图让我的预览处理程序的服务器生命周期正确,如下所述: 使用我们的托管预览处理程序框架查看数据

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

这里描述了进入流程外服务器的方式,对于任何感兴趣的人: 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; } 

.Net框架的工作方式不同,请参阅:
.NET Framework提供的内存管理技术与基于COM的世界中内存管理的工作方式不同。 COM中的内存管理是通过引用计数。 .NET提供了一种涉及参考跟踪的自动内存管理技术。 在本文中,我们将介绍Common Language Runtime CLR使用的垃圾收集技术。

无计可施

[已编辑]更多一轮…

请查看此替代方法将类型库导入为程序集
正如您自己所说的使用CCW,您可以使用传统的COM方式访问reference-counte 。

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

据我所知,这个主题的最佳报道在Alan Gordon的“.NET和COM互操作性手册 ”一书中,该链接应该转到Google Books的相关页面。 (不幸的是我没有它,而是去了Troelsen书 。)

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

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

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

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

在类的构造函数中,使用CoCreateInstance来模拟C#类的实例。 查询IDispatch并保持成员中的指针。

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

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

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

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

我想这不可能的原因是引用计数0并不意味着该对象没有被使用,因为你可能有一个像这样的调用图

 VB_Object | V | Managed1 -<- Managed2 

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

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

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

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

编辑 :这是一些可能以另一种方式描述的链接( 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 

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

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

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

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