是否可以拦截(或了解)对暴露给 COM 的 CLR 对象进行 COM 引用计数
我重新表述了这个问题。
当 .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 可调用包装器变为零?
和/或
是否可以提供我的 AddRef 的实现? .net 中 CCW 的 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 解决方案。
感谢大家。
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.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(10)
我意识到这是一个有点老的问题,但我确实在一段时间前收到了实际的工作请求。
它的作用是将所创建对象的 VTBL 中的 Release 替换为自定义实现,该自定义实现在所有引用都被释放时调用 Dispose。请注意,不能保证这始终有效。主要假设是标准CCW的所有接口上的所有Release方法都是相同的方法。
使用风险自负。 :)
I realize this is somewhat old question, but I did get the actual request to work some time back.
What it does is replace Release in the VTBL(s) of the created object with a custom implementation that calls Dispose when all references have been released. Note that there are no guarantees to this will always work. The main assumption is that all Release methods on all interfaces of the standard CCW are the same method.
Use at your own risk. :)
好的,伙计们,这是另一种尝试。实际上,您可以使用“Windows 脚本组件”来包装 .NET COM 对象并以这种方式完成。这是使用简单的 .NET 计算器的完整示例,可以添加值。我相信您会从那里得到这个概念,这完全避免了 VB-Runtime、ATL 问题,并使用每个主要 WIN32/WIN64 平台上都可用的 Windows 脚本主机。
我在名为 DemoLib 的命名空间中创建了一个名为 Calculator 的简单 COM .NET 类。请注意,这实现了 IDisposable,出于演示目的,我在屏幕上放置了一些内容以显示它已终止。为了简单起见,我在 .NET 和脚本中完全坚持使用 vb,但 .NET 部分可以是 C# 等。保存此文件时,您需要使用 regsvr32 注册它,需要保存它例如 CalculatorLib.wsc 之类的东西。
接下来,我创建一个名为 Calculator.Lib 的 Windows 脚本组件,它有一个方法返回公开 .NET 数学库的 VB-Script COM 类。在这里,我在构造和销毁期间在屏幕上弹出一些内容,请注意,在销毁中我们调用 .NET 库中的 Dispose 方法来释放那里的资源。请注意使用 Lib() 函数将 .NET Com Calculator 返回给调用者。
最后,将所有内容结合在一起,这是一个示例 VB 脚本,其中您可以看到显示创建、计算、在 .NET 库中调用的处理的对话,最后在公开 .NET 组件的 COM 组件中终止。
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.
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.
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.
我还没有验证这一点,但这是我会尝试的:
首先,这是一个 关于 IMarshal 的 clr 默认实现的 CBrumme 博客文章。如果您的实用程序跨 COM 单元使用,您将无法从 VB6 直接端口到 CLR 获得正确的 com 行为。 CLR 实现的 Com 对象的行为就好像它们聚合了自由线程编组器,而不是 VB6 公开的单元线程模型。
您可以实现 IMarshal(在您作为 com 对象公开的 clr 类上)。我的理解是,这将允许您控制创建 COM 代理(而不是互操作代理)。我认为这将允许您捕获从 UnmarshalInterface 返回的对象中的 Release 调用,并向原始对象发出信号。我将包装标准编组器(例如 pinvoke CoGetStandardMarshaler)并转发对它的所有调用。我相信该对象的生命周期将与 CCW 的生命周期相关联。
再次...如果我必须用 C# 解决它,我会尝试这样做。
另一方面,这种解决方案真的比在 ATL 中实现更容易吗?仅仅因为神奇的部分是用 C# 编写的,并不会使解决方案变得简单。如果我上面的建议确实解决了问题,您将需要写一篇非常长的评论来解释发生了什么。
I haven't verified this, but here is what I would try:
First, here is a CBrumme Blog article about the clr's default implementation of IMarshal. If your utilities are used across COM apartments you won't get proper com behavior from a direct port of VB6 to the CLR. Com objects implemented by the CLR act as if they aggregated the free threaded marshaller rather than apartment threaded model that VB6 exposed.
You can implement IMarshal (on the clr class you are exposing as a com object). My understanding is that will allow you control over creating the COM proxy (not the interop proxy). I think this will allow you to trap the Release calls in the object you returned from UnmarshalInterface and signal back to your original object. I'd wrap the standard marshaller (e.g. pinvoke CoGetStandardMarshaler) and forward all calls to it. I believe that object will have a lifetime tied to the lifetime of the the CCW.
again ... this is what I'd try if I had to solve it in C#.
On the other hand, would this kind of solution really be easier than implementing in ATL? Just because the magic part is written in C# doesn't make the solution simple. If what I propose above does solve the problem, you'll need to write a really big comment explaining what was going on.
我也一直在努力解决这个问题,试图让我的预览处理程序的服务器生命周期正确,如下所述:
使用我们的托管预览处理程序框架以您的方式查看数据
我需要获取它进入进程外服务器,突然我遇到了生命周期控制问题。
对于感兴趣的人,这里描述了进入进程外服务器的方法:
RegistrationSrvices.RegisterTypeForComClients社区内容
这意味着您可以通过实现 IDispose 来做到这一点,但这不起作用。
我尝试实现一个终结器,这最终导致对象被释放,但由于服务器调用我的对象的使用模式,这意味着我的服务器永远挂起。我还尝试剥离一个工作项目,并在睡眠后强制进行垃圾收集,但这确实很混乱。
相反,它归结为挂钩 Release(和 AddRef,因为 Release 的返回值不可信)。
(通过这篇文章找到:http://blogs .msdn.com/b/oldnewthing/archive/2007/04/24/2252261.aspx#2269675)
这是我在对象的构造函数中所做的操作:
I've been struggling with this too, to try to get server lifetime correct for my Preview Handler, as described here:
View Data Your Way With Our Managed Preview Handler Framework
I needed to get it into an out of process server, and suddenly I had lifetime control issues.
The way to get into an out of process server is described here for anyone interested:
RegistrationSrvices.RegisterTypeForComClients community content
which implies that you may be able to do it by implmenting IDispose, but that didn't work.
I tried implementing a finalizer, which did eventually cause the object to be released, but because of the pattern of usage of the server calling my object, it meant my server hung around forever. I also tried spinning off a work item, and after a sleep, forcing a garbage collect, but that was really messy.
Instead, it came down to hooking Release (and AddRef because the return value of Release couldn't be trusted).
(Found via this post: http://blogs.msdn.com/b/oldnewthing/archive/2007/04/24/2252261.aspx#2269675)
Here's what I did in my object's constructor:
.Net 框架的工作方式有所不同,请参阅:
.NET 框架提供与基于 COM 的世界中内存管理工作方式不同的内存管理技术。 COM 中的内存管理是通过引用计数进行的。 .NET 提供了一种涉及引用跟踪的自动内存管理技术。在本文中,我们将了解公共语言运行时 CLR 使用的垃圾收集技术。
没什么可做的
[已编辑]又一轮...
看看此替代方案将类型库作为程序集导入
正如您自己所说,使用 CCW 您可以以传统 COM 方式访问引用计数。
[编辑]坚持是一种美德
你知道WinAPIOverride32吗?有了它,您可以捕获并研究它的工作原理。
另一个可以提供帮助的工具是 Deviare COM Spy Console。
这并不容易。
祝你好运。
.Net framework works differently, see:
The .NET Framework provides memory management techniques that differ from the way memory management worked in a COM-based world. The memory management in COM was through reference counting. .NET provides an automatic memory management technique that involves reference tracing. In this article, we'll take a look at the garbage collection technique used by the Common Language Runtime CLR.
nothing to be done
[EDITED] more one round...
Take a look at this alternative Importing a Type Library as an Assembly
As you yourself said using CCW you can access reference-counte in traditional COM fashion.
[EDITED] Persistence is a virtue
You know WinAPIOverride32? With it you can capture and study how it works.
Another tool that can help is Deviare COM Spy Console.
This will not be easy.
Good luck.
据我所知,有关此主题的最佳内容在本书The .NET and COM Interoperability Handbook 作者:Alan Gordon,该链接应转至 Google 图书中的相关页面。 (不幸的是我没有它,我去了 Troelsen 书 .)
那里的指导意味着没有明确定义的方法来挂钩 CCW 中的
发布
/引用计数。相反,建议您将 C# 类设置为一次性的,并鼓励您的 COM 客户端(在您的例子中是 VBScript 作者)在希望发生确定性终结时调用Dispose
。但幸运的是,您有一个漏洞,因为您的客户端是后期绑定 COM 客户端,因为 VBScript 使用
IDispatch
来对对象进行所有调用。假设您的 C# 类是通过 COM 公开的。首先让其发挥作用。
现在,使用 ATL 简单对象向导在 ATL/C++ 中创建一个包装类,并在选项页面中选择 Interface: Custom 而不是 Dual。这会阻止向导添加自己的 IDispatch 支持。
在类的构造函数中,使用 CoCreateInstance 来神奇地创建 C# 类的实例。查询 IDispatch 并在成员中保留该指针。
将
IDispatch
添加到包装类的继承列表中,并将IDispatch
的所有四个方法直接转发到您隐藏在构造函数中的指针。在包装器的
FinalRelease
中,使用后期绑定技术 (Invoke
) 调用 C# 对象的Dispose
方法,如艾伦·戈登的书(在我上面链接的页面上)。现在,您的 VBScript 客户端正在通过 CCW 与 C# 类进行通信,但您可以拦截最终版本并将其转发到
Dispose
方法。让您的 ATL 库为每个“真正的”C# 类公开一个单独的包装器。您可能希望使用继承或模板来获得良好的代码重用。您支持的每个 C# 类应该只需要 ATL 包装代码中的几行。
As far as I'm aware, the best coverage of this subject is in the book The .NET and COM Interoperability Handbook By Alan Gordon, and that link should go to the relevant page in Google Books. (Unfortunately I don't have it, I went for the Troelsen book instead.)
The guidance there implies that there isn't a well-defined way of hooking into the
Release
/reference counting in the CCW. Instead the suggestion is that you make your C# class disposable, and encourage your COM clients (in your case the VBScript authors) to callDispose
when they want deterministic finalisation to occur.But happily there is a loophole for you because your clients are late-binding COM clients, because VBScript uses
IDispatch
to make all calls to objects.Suppose your C# classes were exposed via COM. Get that working first.
Now in ATL/C++ create a wrapper class, using the ATL Simple Object wizard, and in the options page choose Interface: Custom instead of Dual. This stops the wizard putting in its own
IDispatch
support.In the class's constructor, use CoCreateInstance to magic up an instance of your C# class. Query it for
IDispatch
and hold onto that pointer in a member.Add
IDispatch
to the wrapper class's inheritance list, and forward all four methods ofIDispatch
straight through to the pointer you stashed away in the constructor.In the
FinalRelease
of the wrapper, use the late binding technique (Invoke
) to call theDispose
method of the C# object, as described in the Alan Gordon book (on the pages I linked to above).So now your VBScript clients are talking via the CCW to the C# class, but you get to intercept the final release and forward it to the
Dispose
method.Make your ATL library expose a separate wrapper for each "real" C# class. You'll probably want to use inheritance or templates to get good code reuse here. Each C# class you support should only require a couple of lines in the ATL wrapping code.
我猜这是不可能的原因是引用计数为 0 并不意味着该对象未在使用中,因为您可能有一个调用图,
在这种情况下,即使 VB 对象删除,对象 Managed1 仍在使用中因此,它对它的引用及其引用计数为 0。
如果您确实需要按照您所说的操作,我想您可以在非托管 C++ 中创建包装类,当引用计数降至 0 时,它会调用 Dispose 方法。这些类可能是 codegen来自元数据,但我对如何实现此类事情没有任何经验。
I guess the reason for this not being possible is that a refcount of 0 does not mean that the object is not in use, because you might have a call graph like
in which case the object Managed1 is still in use even if the VB object drops its reference to it and its refcount therefore is 0.
If you really need to do what you say, I guess you could create wrapper classes in unmanaged C++, which invokes the Dispose method when the refcount drops to 0. These classes could probably be codegen'd from metadata, but I have no experience whatsoever in how to do implement this kind of thing.
从 .NET 请求对象上的 IUnknown。调用 AddRef(),然后调用 Release()。然后获取 AddRef() 的返回值并使用它运行。
From the .NET, request an IUnknown on the object. Call AddRef(), then Release(). Then take the return value of AddRef() and run with it.
为什么不转变范式。围绕暴露和扩展通知方法创建您自己的聚合怎么样?它甚至可以在.Net 中完成,而不仅仅是ATL。
编辑:
这是一些可能以另一种方式描述的链接(http ://msdn.microsoft.com/en-us/library/aa719740(VS.71).aspx)。但以下步骤解释了我上面的想法。
使用单一方法创建新的 .Net 类来实现您的旧接口 (ILegacy) 和新接口 (ISendNotify):
在 MyClass 内部创建真正的旧对象的实例,并将来自 MyClass 的所有调用委托给该实例。这是一个聚合。因此聚合的生命周期现在取决于 MyClass。由于 MyClass 是 IDisposable 现在您可以在实例被删除时进行拦截,因此您可以通过 IMyListener
EDIT2 发送通知:在那里(http://vb.mvps.org/hardcore/html/countingreferences.htm) IUnknown 发送事件的最简单实现
Why don't shift paradigm. What about to create your own aggregate around exposed and extend with notification methods. It even can be done in .Net not only by ATL.
EDITED:
Here is some link that may be describe some another way(http://msdn.microsoft.com/en-us/library/aa719740(VS.71).aspx). But following steps explains my idea above.
Create create new .Net class that implements your legacy interface(ILegacy), and new interface (ISendNotify) with single method:
Inside the MyClass create instance of your real legacy object, and delegate all calls from MyClass to this instance. This is an aggregation. So lifetime of aggregate now depends on MyClass. Since MyClass is a IDisposable now you can intercept when instance is deleted, so you can send notification by IMyListener
EDIT2: Taken there (http://vb.mvps.org/hardcore/html/countingreferences.htm) simplest impl of IUnknown with sending event
据我所知,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执行而不冻结应用程序线程而不改变其基本操作。)
To my knowledge, the GC already provides support for what you are trying to do. It is called Finalization. In a purely managed world, best practice is to avoid Finalization, as it has some side effects that can negatively impact the performance and operation of the GC. The IDisposable interface provides a clean, managed way of bypassing object finalization and providing cleanup of both managed and unmanaged resources from within managed code.
In your case, you need to initiate cleanup of a managed resource once all unmanaged references have been released. Finalization should excel at solving your problem here. The GC will finalize an object, always, if a finalizer is present, regardless of how the last references to a finalizable object were released. If you implement a finalizer on your .NET type (just implement a destructor), then the GC will place it in the finalization queue. Once the GC collection cycle is complete, it will process the finalization queue. Any cleanup work you perform in your destructor will occur once the finalization queue is processed.
It should be noted that if your finalizable .NET type contains references to other .NET objects which in turn require finalization, you could invoke a lengthy GC collection, or some of the objects may survive for longer than they would without finalization (which would mean they survive a collection and reach the next generation, which is collected less frequently.) However, if the cleanup work of your .NET objects that use CCW's is not time sensitive in any fashion, and memory usage is not a huge issue, some extra lifetime shouldn't matter. It should be noted that finalizable objects should be created with care, and minimizing or eliminating any class instance level references to other objects can improve your overall memory management via the GC.
You can read more about finalization in this article: http://msdn.microsoft.com/en-us/magazine/bb985010.aspx. While it is a rather old article from back when .NET 1.0 was first released, the fundamental architecture of the GC is unchanged as of yet (the first significant changes to the GC will be arriving with .NET 4.0, however they are related more to concurrent GC execution without freezing the application threads than changes to its fundamental operation.)