如何将 COM 对象包装在本机 .NET 类中?
我在 .NET (C#) 中使用广泛的现有 COM API(可能是 Outlook,但事实并非如此)。我通过在 Visual Studio 中添加“COM 引用”来完成此操作,因此所有“魔法”都是在幕后完成的(即,我不必手动运行 tlbimp)。
虽然现在可以从 .NET“轻松”使用 COM API,但它对 .NET 不太友好。例如,没有泛型,事件很奇怪,像 IPicture 等。因此,我想创建一个使用现有 COM API 实现的本机 .NET API。
简单的第一遍可能是
namespace Company.Product {
class ComObject {
public readonly global::Product.ComObject Handle; // the "native" COM object
public ComObject(global::Product.ComObject handle) {
if (handle == null) throw new ArgumentNullException("handle");
Handle = handle;
}
// EDIT: suggestions from nobugz
public override int GetHashCode() {
return Handle.GetHashCode();
}
public override bool Equals(object obj) {
return Handle.Equals(obj);
}
}
}
这种方法的一个直接问题是,您可以轻松地为同一个底层“本机 COM”对象获得多个 ComObject 实例。例如,在进行枚举时:
IEnumerable<Company.Product.Item> Items {
get {
foreach (global::Item item in Handle.Items)
yield return new Company.Product.Item(item);
}
}
在大多数情况下这可能是意想不到的。解决这个问题可能看起来
namespace Company.Product {
class ComObject {
public readonly global::Product.ComObject Handle; // the "native" COM object
static Dictionary<global::Product.ComObject, ComObject> m_handleMap = new Dictionary<global::Product.ComObject, ComObject>();
private ComObject(global::Product.ComObject handle) {
Handle = handle;
handleMap[Handle] = this;
}
public ComObject Create(global::Product.ComObject handle) {
if (handle == null) throw new ArgumentNullException("handle");
ComObject retval;
if (!handleMap.TryGetValue(handle, out retval))
retval = new ComObject(handle);
return retval;
}
}
}
更好。枚举器更改为调用 Company.Product.Item.Create(item)
。 但现在的问题是 Dictionary 将使两个对象保持“活动”状态,因此它们永远不会被垃圾收集;这可能对 COM 对象不利。现在事情开始变得混乱......
它看起来像是解决方案的一部分 正在使用 在某种程度上弱引用。还有关于使用 IDisposable 的建议 但必须在每个对象上处理 Dispose() 似乎对 .NET 不太友好。然后还有关于何时/如果的各种讨论应该调用ReleaseComObject。 代码位于 http://codeproject.com 使用后期绑定,但我对依赖于版本的 API 感到满意。
因此,目前我不太确定最好的继续方式是什么。我希望我的本机 .NET API 尽可能“类似于 .NET”(甚至可能将 Interop 程序集嵌入到 .NET 4.0),并且不必采用“两点”规则等启发式方法。
我想到尝试的一件事是创建一个 ATL 项目,使用 /clr 标志进行编译并使用 C++ 的编译器 COM 支持(由 创建的 Product::ComObjectPtr #import) 而不是 .NET RCW。当然,我通常宁愿C# 代码 优于 C++/CLI...
I'm using an extensive existing COM API (could be Outlook, but it's not) in .NET (C#). I've done this by adding a "COM Reference" in Visual Studio so all the "magic" is done behind the scenes (i.e., I don't have to manually run tlbimp).
While the COM API can now be "easily" used from .NET, it is not very .NET friendly. For example, there are no generics, events are strange, oddities like IPicture, etc. So, I'd like to create a native .NET API that is implemented using the existing COM API.
A simple first pass might be
namespace Company.Product {
class ComObject {
public readonly global::Product.ComObject Handle; // the "native" COM object
public ComObject(global::Product.ComObject handle) {
if (handle == null) throw new ArgumentNullException("handle");
Handle = handle;
}
// EDIT: suggestions from nobugz
public override int GetHashCode() {
return Handle.GetHashCode();
}
public override bool Equals(object obj) {
return Handle.Equals(obj);
}
}
}
One immediate problem with this approach is that you can easily end up with multiple instances of ComObject for the same underlying "native COM" object. For example, when doing an enumeration:
IEnumerable<Company.Product.Item> Items {
get {
foreach (global::Item item in Handle.Items)
yield return new Company.Product.Item(item);
}
}
This would probably be unexpected in most situations. Fixing this problem might look like
namespace Company.Product {
class ComObject {
public readonly global::Product.ComObject Handle; // the "native" COM object
static Dictionary<global::Product.ComObject, ComObject> m_handleMap = new Dictionary<global::Product.ComObject, ComObject>();
private ComObject(global::Product.ComObject handle) {
Handle = handle;
handleMap[Handle] = this;
}
public ComObject Create(global::Product.ComObject handle) {
if (handle == null) throw new ArgumentNullException("handle");
ComObject retval;
if (!handleMap.TryGetValue(handle, out retval))
retval = new ComObject(handle);
return retval;
}
}
}
That looks better. The enumerator changes to call Company.Product.Item.Create(item)
.
But now the problem is the Dictionary<> will keep both objects "alive" so they will never be garbage collected; this is likely bad for the COM object. And things start getting messy now...
It looks like part of the solution is using a WeakReference in some way. There are also suggestions about using IDisposable but it doesn't seem very .NET-friendly at all to have to deal with Dispose() on every single object. And then there's the various discussions of when/if ReleaseComObject should be called. There is also code over on http://codeproject.com that uses late binding, but I'm happy with a version-dependent API.
So, at this point I'm not really sure what is the best way to proceed. I'd like my native .NET API to be as ".NET-like" as possible (maybe even embedding the Interop assembly with .NET 4.0) and w/o having to employ heuristics like the "two dots" rule.
One thing I thought of trying is to create an ATL project, compile with the /clr flag and use the C++'s compiler COM support (Product::ComObjectPtr created by #import) rather than .NET RCWs. Of course, I'd generally rather code in C# than C++/CLI...
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
您自己并不是在处理 COM 对象。您已经在处理一个外观,它是在您向项目中添加对 COM 二进制文件的引用时创建的。 (.NET) 将为您生成一个外观,从而将使用 COM 对象的任务简化为仅使用常规 .NET 类。如果您不喜欢为您生成的界面,您可能应该为现有外观创建一个外观。您不必担心 COM 的复杂性,因为这已经为您完成了(可能有一些事情您确实需要担心,但我认为它们很少而且相差很远)。只需将该类用作常规 .net 类,因为它就是这样,并在出现任何问题时进行处理。
编辑:您可能遇到的问题之一是不确定的 COM 对象破坏。幕后发生的引用计数依赖于垃圾收集,因此您无法确定对象何时会被销毁。根据您的应用程序,您可能需要对 COM 对象进行更具确定性的销毁。为此,您可以使用Marshal.ReleaseComObject。 如果是这种情况,那么您应该注意这个问题。
抱歉,我会发布更多链接,但显然如果没有先获得 10 个声誉,我就无法发布超过 1 个链接。
You, yourself aren't dealing with COM objects. You are already dealing with a facade that was created the moment you added a reference to the COM binary to your project. (.NET) will generate a facade for you, therefore simplifying the task of using COM objects to simply using regular .NET classes. If you do not like the interface that's generated for you, you should probably create a facade to the existing facade. You don't have to worry about COM intricacies, because that's already been done for you (there may be some things you do need to worry about, but I think they are few and far between). Just use the class as a regular .net class because that's exactly what it is, and deal with any problems as they arise.
EDIT: One of the problems you might experience is nondeterministic COM object destruction. The reference counting that's taking place behind the scenes relies on garbage collection so you can't be sure when your objects will be destroyed. Depending on your application you may need more deterministic destruction of your COM objects. To do this you would use Marshal.ReleaseComObject. If this is the case, then you should be aware of this gotcha.
Sorry, I would post more links, but apparently I can't post more than 1 without first getting 10 reputation.
我发现将 COM 对象引入 .NET 的最大问题是垃圾收集器在不同的线程上运行,并且 COM 对象的最终版本通常(总是?)从该线程调用。
Microsoft 在这里故意打破了 COM 线程模型规则,该规则规定,对于单元线程对象,所有方法都必须从同一线程调用。
对于某些 COM 库来说,这不是什么大问题,但对于其他库来说,这是一个大问题 - 特别是对于需要在析构函数中释放资源的库。
需要注意的事情...
The biggest problem I've found with bringing COM objects into .NET is the fact that the garbage collector runs on a different thread and the final release of the COM object will often (always?) be called from that thread.
Microsoft deliberately broke the COM threading model rules here which state that with apartment threaded objects, all methods must be called from the same thread.
For some COM libraries this is not a big deal, but for others it's a huge problem - particularly for libraries that need to release resources in their destructors.
Something to be aware of...
你让事情变得不必要的困难。它不是“句柄”,不需要引用计数。 __ComObject 是常规的 .NET 类,并遵守正常的垃圾收集规则。
You are making it unnecessarily difficult. It is not a "handle", there's no need for reference counting. A __ComObject is a regular .NET class and subject to normal garbage collection rules.
听起来您正在寻求围绕复杂的 COM API 创建一个更简单的 .NET API。正如 nobugz 所说,您的 ComObject 类是真实的、本机 .NET 对象,其内部包含对实际 com 对象的非托管引用。您不需要做任何奇怪的事情来管理它们...只需像使用普通的 .NET 对象一样使用它们即可。
现在,关于向 .NET 消费者展示“更漂亮的面孔”。有一个现有的设计模式,它被称为Facade。我假设您实际上只需要这些 COM 对象提供的部分功能。如果是这种情况,请在 com 互操作对象周围创建一个外观层。该层应包含必要的类、方法和支持类型,通过比 com 对象本身更友好的 API 为所有 .NET 客户端提供必要的功能。外观还负责简化奇怪的事情,例如将 com 对象对事件所做的任何操作转换为普通 .NET 事件、数据封送、简化 com 对象创建、设置和拆卸等。
一般来说,应避免抽象,因为它们往往会增加工作量和复杂性。然而有时它们是必要的,并且在某些情况下可以大大简化事情。如果更简单的 API 可以提高需要使用非常复杂的 com 对象系统提供的某些功能的其他团队成员的生产力,那么抽象就可以提供有形的价值。
It sounds like you are looking to create a simpler .NET API around your complex COM API. As nobugz said, your ComObject classes are real, native .NET objects that internally contain the unmanaged references to your actual com objects. You don't need to do anything funky to manage them...just use them like they are normal .NET objects.
Now, in regards to presenting a "prettier face" to your .NET consumers. There is an existing design pattern for this, and its called a Facade. I am going to assume that you only really need a part of the functionality that these COM objects provide. If that is the case, then create a facade layer around your com interop objects. This layer should contain the necessary classes, methods, and support types that provide the neccesary functionality to all of the .NET clients with a friendlier API than the com objects have themselves. The facade would also be responsible for simplifying oddities like converting whatever the com objects do for events with normal .NET events, data marshaling, simplification of com object creation, setup, and teardown, etc.
While in general, abstractions should be avoided, as they tend to add work and complexity. However sometimes they are necessary, and in some cases can greatly simplify things. If a simpler API can improve the productivity of other team members who need to consume some of the functionality provided by a very complex com object system, then an abstraction provides tangible value.