COM与.NET的交互
.NET framework 是从COM的一种自然地进步,因为这两个模型共享了许多中心的主题,包括组件重用和语言中立。为了支持向后兼容,COM interop提供了不需要修改现有组件而能访问现有COM组件的方法。可以通过使用COM interop工具导入相关的COM类型来合并COM组件到.NET Framework的应用中。一旦导入,COM的类型就可以使用了。
COM interop 同时也提供了向前兼容使得COM的客户可以像访问其他的COM对象一样访问托管的代码,COM interop又一次的提供了所谓的无缝从程序集中导出元数据(metadata)到类型库并且像传统COM组件一样注册托管组件的方法。无论是导出还是导入工具处理的结果都与COM规范一致。在运行时,如果需要的话common language runtime在COM对象和托管代码之间列集(marshals)数据
1. COM Wrappers
COM在以下几个方面与.NET Framework的对象模型有所不同:
• COM对象的客户程序必须管理这些对象的生命期;在.NET Framework 中CLR管理这些对象的生命期
• COM的客户通过请求一个接口并得到接口的指针来查询一个服务是否有效,.NET的客户可以通过反射(reflection)来得到一个对象的功能的描述
• .NET的对象驻留在.NET Framework执行环境管理的内存中,执行环境可以因为性能的原因删除内存中的对象并且更新它删除的对象的所有引用。非托管的客户,得到一个对象的指针,依赖于对象保留在相同的位置。这种客户没有那种处理在内存中不在固定位置的对象的机制。
为了克服这些不同,runtime提供了包装类使得托管代码和非托管代码的客户都认为他们在自己的环境中调用对象的方法。当托管客户调用一个COM对象的方法时,runtime创建一个runtime callable wrapper (RCW)。RCWs抽象了托管代码和非托管代码引用机制的不同。Runtime还创建了一个COM callable wrapper (CCW)来实现其逆过程,使得COM的客户能够无缝的调用.NET对象的方法。如下图所示
COM wrapper overview
在大多数情况下,标准的RCW或者CCW由runtime生成,为跨越COM和.NET Framework的边界调用提供了足够的列集。使用自定义的属性,可以随意的调整runtime表示托管代码和非托管代码的方式
Runtime Callable Wrapper
CLR(common language runtime)通过一个叫做runtime callable wrapper (RCW)的代理(proxy)暴露COM 对象. 虽然RCW对于.NET的客户似乎是一个普通的.NET对象,但是它的主要功能却是在.NET客户和COM对象之间列集调用。
运行时正确的为每个COM对象创建一个RCW,而不管那个对象上存在的引用的个数。如下图所示:任意数量的托管客户可以保持一个暴露INew 和INewer 接口的COM对象的引用。运行时为每个COM对象维护一个单独的RCW。
Accessing COM objects through the runtime callable wrapper
使用来源于类型库(type library)的元数据(metadata),运行库(runtime)创建将被调用COM对象和这个对象的包装(wrapper)。每个RCW维护它所包装的COM对象的接口指针并且在RCW不再需要时释放COM对象。运行库(runtime)在RCW之上作垃圾收集。
在其他的活动中,RCW代表所包装的对象在托管和非托管代码之间列集(marshals)数据。特别的是,RCW在客户和服务有不同的数据表现形式,并需要在他们之间传递数据时,提供方法参数和方法返回值得列集。
标准的wrapper执行内置(built-in)的列集规则。例如:当.NET的客户传递一个String类型作为参数的一部分给托管对象的时候,wrapper把string类型那个转化为BSTR类型。当COM对象返回一个BSTR给托管的调用着的时候,调用着收到一个string。无论是客户还是服务端接受和发送数据都使用自己熟悉的类型。还有一些其他的类型不需要变化,例如:一个标准的wrapper将在托管和非托管代码之间一直传递4-byte integer而不做任何变化。
RCW的主要目标就是隐藏托管代码模型和非托管代码模型之间的差别,实现无缝的传输。RCW使用选择的接口而不把它们暴露给.NET的客户端,如下图所示
COM interfaces and the runtime callable wrapper
当创建一个早期的COM对象的时候,RCW是一个特殊的类型。它实现了COM对象实现的一些接口并且暴露它们的方法,属性和时间。如图所示:RCW暴露了INew接口,但是使用了IUnknown 和IDispatch接口。RCW向.NET客户暴露了INew的所有成员
COM Callable Wrapper
当COM的客户调用.NET的对象时,CLR创建这个托管的对象和这个托管的对象COM callable wrapper(CCW),COM客户可以使用CCW作为托管对象的一个代理,而不能够直接使用.NET的对象。
Runtime正确的创建托管对象的CCW,不管要求这个服务的COM客户的数量。如下图所示,多个COM客户可以保持包含INew 接口的CCW的引用,CCW,反过来包含一个实现了接口和垃圾收集的托管对象的单独的引用。COM和.NET的客户可以同时在相同的托管对象是发起调用。
Accessing .NET objects through COM callable wrapper
COM callable wrappers对于运行在.NET Framework上的其他类是不可见的。它们的主要目的是在托管和非托管代码之间列集(marshal)调用,而且CCWs同时也管理着它所包含对象的identity和对象的生存期(lifetime)
Object Identity
运行时(runtime)在它的能够垃圾回收的堆里(garbage-collected heap)为.NET对象分配内存,这能使runtime需要的时候在内存中移动对象。相反,runtime在不能进行垃圾收集的堆上为CCW分配内存,使COM客户能够直接引用它。
Object Lifetime
与它所包含的.NET 对象不同,CCW是一个基于引用计数的传统的COM。当CCW的引用计数减少到0,wrapper释放自己在托管对象上的引用。没有引用的托管的对象在下一次垃圾收集的周期内将被收集。
Customizing Standard Wrappers
这部分将描述如何自定义标准的runtime callable wrappers(RCW) and COM callable wrappers.(CCW)
Runtime Callable Wrappers
当.NET激活一个COM对象时,运行时(runtime)生成一个包含COM类型的runtime callable wrapper (RCW),如下图所示, 运行时(runtime)使用从导入的类型库而得到的元数据(metadata)来生成RCW。Wrapper根据interop marshaling service定义的规则列集数据。
RCW generation and method calls
有两个方式可以自定义RCW。如果你可以修改Interface Definition Language (IDL)的源文件,你可以给type library file (TLB) 添加属性然后导入TLB。还可以应用interop-specific attributes来导入类型生成新的程序集(assembly)支持自定义的标准RCWs被这些attributes限制。
To modify the IDL source
1. Apply TLB attributes to libraries, types, members, and parameters. Use the custom keyword and an attribute value to change metadata. By applying TLB attributes, you can:
• Specify the managed name of an imported COM type, instead of allowing the import utility to select the name according to standard conversion rules.
• Explicitly define a destination namespace for the types in a COM library.
2. Compile the IDL source code.
3. Generate an assembly from the resulting type library file or from a dynamic link library file (DLL) that contains the type you intend to implement.
To modify an imported assembly
1. Import the type library file. Use the Type Library Importer (Tlbimp.exe) to generate an assembly DLL.
2. Create a text file from the imported assembly by using the MSIL Disassembler (Ildasm.exe).
3. Apply interop attributes to the text file.
4. Generate a new assembly from the modified text file by using the MSIL Assembler (Ilasm.exe).
COM Callable Wrappers
COM callable wrapper (CCW) 向COM导出NET Framework对象. 通过把一个托管的工程编译成一个DLL的程序集, 可以自动的创建需要的元数据来描述程序集中的类型. 当一个COM的客户激活托管对象时,运行时使用元数据来生成CCW.
自定义CCW,你的托管的代码要遵循交互规范属性(interop-specific attributes),并且把编译代码编译成程序集,如下图所示,在这个例子中,Tlbexp.exe把托管类型编译为COM
CCW 的生成和方法调用
通过在代码中添加属性,可以在交互列集服务限定的范围内改变接口和数据的列集行为。例如,可以你可以控制方法参数传递的格式,也可以控制程序集中的什么类型暴露给COM
2 .托管和非托管的线程(Managed and Unmanaged Threading)
COM组件使用套间(apartments)来同步资源的访问。与之对应的是,托管对象使用同步区域(synchronized regions),同步原语例如互斥量(mutexes)锁定和完成异步端口,同步上下文来保证所有的共享资源以线程安全的方式被使用。
对于可交互性来说,CLR(common language runtime)在调用COM对象时,创建并初始化一个套间,一个托管的线程可以创建并且进入一个包含一个线程的single-threaded apartment (STA)或者包含多个线程的multi-threaded apartment (MTA)。当COM的套间和线程的套间兼容的时候,COM允许调用的线程直接调用COM对象的方法,如果套间不兼容,COM创建兼容的套间并通过代理列集(marshals)所有的调用。
在第一个对非托管代码的调用时,运行时调用CoInitializeEx来初始化MTA或者STA的COM套间。可以使用System.Threading.ApartmentState属性来控制创建的套间的类型是MTA, STA, 或者Unknown.在代理存根或者TLB已经注册后,不一定义定设定要设定这个属性。
下表列出了ApartmentState的枚举值和对应的COM套间初始化调用
ApartmentState enumeration value
|
COM apartment initialization
|
MTA
|
CoInitializeEx(NULL, COINIT_MULTITHREADED)
|
STA
|
CoIntializeEx(NULL, COINIT_APARTMENTTHREADED)
|
Unknown
|
CoInitializeEx(NULL, COINIT_MULTITHREADED)
|
当COM对象和托管线程在不兼容的套间时,所有的调用都通过COM创建的代理,下面的代码例子显示了怎么在托管代码重创建一个STA套间模型的COM对象AptSimple
[C#]
using System.Threading;
using APTOBJLib;
...
AptSimple obj = new AptSimple ();
obj.Counter = 1;
为了消除代理存根,显著的提高性能,注意创建对象之前的ApartmentState
[C#]
using System.Threading;
using APTOBJLib;
...
Thread.CurrentThread.ApartmentState = ApartmentState.STA;
AptSimple obj = new AptSimple ();
obj.Counter = 1;
设定套间状态之后,可以向下面那样通过程序查询状态
[C#]
Thread.CurrentThread.ApartmentState = ApartmentState.STA;
if (Thread.CurrentThread.ApartmentState == ApartmentState.STA) {
// All is OK.
}
else {
// Incompatible apartment state.
}
3 托管和非托管的事件(Managed and Unmanaged Events)
.NET Framework的事件模型与传统的COM的事件模型不同。托管的事件模型基于委托(delegate),er非托管的事件(在COM中)基于连接点(connection points)。两个模型都是紧耦合的事件系统,因为客户(事件接受者)和服务(事件发送者)必须同时的运行。
这一部分描述了怎样过渡托管和非托管的事件系统,使得对象可以跨域交互边界发送和接收事件。
COM 事件
这部分提供关于连接点的概要介绍以及用来说明COM事件相关的通用术语
连接点在客户和COM的服务器之间确立了一种双向de通信机制。通过这种机制,COM服务器在事件发生时可以回调客户。例如,服务器(像Microsoft Internet Explorer)可以发出一个事件来向客户程序报告一个变化(例如标题变化)。客户创建了一个叫做event sink内部的COM对象来响应通知,当收到通知,客户可以执行事件相关的操作。
event sink提供了向服务器暴露事件相关方法的接口。服务器通过这些事件相关的方法激发事件。客户像实现普通的COM接口一样实现event sink接口。服务器声明这个接口为出接口,COM服务的作者在类型库中队这个接口应用source 属性。服务器使用event sink接口的定义来确定sink并且invoke方法
实现了event sink接口的COM客户通常叫做event sink,或者简单的称为sink。
在下图中,sink实现了ISinkEvents接口,服务器可以激发事件
连接点事件模型(Connection point event model)
event sink的接口确定之后,sink必须与源对象建立连接,连接点的机制使用下面的过程连接sink和source:
1. The sink queries a server object for the IConnectionPointContainer interface. If the object supports connection points, it returns a pointer.
2. Using methods on the container object, the sink locates the IConnectionPoint interface representing a specific connection point. Since a server can support multiple outgoing interfaces, a client must match its sink to the interface identifier (IID) of a particular connection point interface.
3. Having obtained the correct connection point object, the sink calls IConnectionPoint::Advise to register its sink interface pointer. The server (source) holds the connection (and raises events to it) until the client breaks the connection by calling IConnectionPoint::Unadvise.
处理COM源发出的事件(Handling Events Raised by a COM Source)
如果你不熟悉.NET Framework提供的基于委托(delegate-based)的事件模型,参考Handling and Raising Events。
An imported delegate signature comprises the sink event interface, an underscore, the event name, and the word EventHandler: SinkEventInterface_EventNameEventHandler.
.NET的客户(event sink)可以接受现存的COM服务器(event source)的事件。COM interop在你的托管客户的元数据中产生必要的委托。一个导入的委托签名(signature)由sink event接口,一个下划线,事件名称和EventHandler组成
与现存的COM事件源交互(To interoperate with an existing COM event source)
1. Obtain the primary interop assembly for the COM server if the COM types are to be shared by other applications. A primary interop assembly contains metadata representing the converted type library and is signed by the publisher.
Note If the primary interop assembly is not available or if the assembly is to be used privately, you can import the type library by using Tlbimp.exe or an equivalent API.
The conversion process generates a delegate for each event; however, you only have to sink the events that interest you.
2. You can use a metadata browser, such as Ildasm.exe, to identify events delegates.
3. Consume events from the COM event source the same way you consume events from a managed event source.
下面的例子说明了怎样打开一个Internet Explorer窗口,得到Internet Explorer对象发出的事件并在托管的代码中处理。Internet Explorer的类型(包括事件委托)的定义从被SHDocVw.dll导入为元数据,例子激发TitleChange事件