我已经改写了这个问题。
当.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与我们集成的客户提供的实用程序和帮助程序函数。
我不想做什么
感谢
BW
接受的答案
感谢Steve Steiner感谢Earwicker,他提出了唯一的(可能可行的)基于.net的答案,而Bigtoe提出了一个非常简单的ATL解决方案。
然而,接受的答案是{{3}},他建议将.net对象包装在VbScript对象中(我认为不诚实),有效地为VbScript问题提供了简单的VbScript解决方案。
感谢所有人。
答案 0 :(得分:5)
我在名为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)
无所事事
[已编辑]更多一轮...
看看这个替代Importing a Type Library as an Assembly
就像你自己说的那样使用CCW you can access reference-counte in traditional COM fashion。
[编辑]坚持是一种美德 你知道WinAPIOverride32吗?有了它,您可以捕捉并研究它的工作原理。 另一个可以提供帮助的工具是Deviare COM Spy Console 这并不容易 祝你好运。
答案 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执行而不冻结应用程序线程而不是更改其基本操作。)