大家在进行COM Interop编程的时候,不知道]是否会见到这样的情况。通常,我们通过TlbImp.exe把一个类型库(Type Library)转换成Interop Assembly。比如在Type Library里面有一个coclass叫做MyComObject,那么在Interop Assembly中也存在一个MyComObjectClass这样一个托管类型。用户可以直接使用这个MyComObjectClass操作MyComObject这样一个COM对象,比如使用new创建,调用方法,等等。因为MyComObjectClass并不是MyComObject这个COM对象本身,而是像一个代理(Proxy),.NET中我们将其称为RCW (Runtime Callable Wrapper)。但是在有些情况下,在使用某些函数的时候,理论上应该返回一个MyComObjectClass,然而实际返回的却是一个System.__ComObject类型,这是什么原因呢?
事实上,RCW是存在两种类型的,一种是强类型(Strongly Typed)的RCW,另外一种是弱类型的RCW,即System.__ComObject。
所谓强类型的RCW,也就是说这种RCW具有关于这个COM对象实现了那些接口(Interface),有哪些方法等等各种信息,这些信息都在元数据(Metadata)里面,因此你一旦看到这个RCW,就知道这个RCW是何种COM对象。通常情况下,大家遇到的都是强类型的RCW,这主要是因为TlbImp对强类型的RCW提供了良好的支持。比如上面所提到的MyComObjectClass便是TlbImp生成的一个强类型RCW。对COM编程有基本了解的朋友应该非常清楚,对于COM对象而言,你在操作COM对象的时候(除了调用CoCreateInstanc创建的时候),大部分时候都是不清楚你在和具体哪个COM对象打交道,唯一的信息只是接口。那么.NET/CLR又是如何做到这一点的呢?当然了,我们无需考虑从.NET代码中直接创建强类型RCW的情形,以及从一个托管方法返回一个强类型RCW的情形,因为这两种情况没有歧义,很显然得到的结果均是一个强类型的RCW。我们主要考虑的是,在非托管方法中返回一个接口,CLR是如何知道这个接口是何种对象的。这种情况是最复杂的。
还是举一个例子:假设MyComObject对象实现了ITest接口。然后托管代码调用某个非托管函数(可以是COM对象/接口的函数,也可以是P/Invoke)的原型如下:
ITest *GetTest();
我们首先分析一下,如果用TlbImp来Import这个函数,对应的托管函数的原型是什么。有两种情况:
1. ITest是MyComObject的缺省的接口(default interface),并且没有其他coclass把ITest作为缺省的接口使用。这种情况下,TlbImp会将ITest转变为MyComObject接口。注意这个接口是由TlbImp生成的一个接口,而非type library本身里面所具有的,当然更不是这个MyComObject对象(MyComObject对象在TlbImp所生成的Interop Assembly中对应的类型是MyComObjectClass)。这个接口可以认为代表了这个COM对象本身,并且TlbImp会用CoClassAttribute这个属性把两者关联起来。因此最后的结果是:
MyComObject *GetTest();
2. 否则,TlbImp不做任何改变,直接使用原有的函数原型:
ITest *GetTest();
情况#1其实是最简单的情况。原因是,MyComObject接口因为上面标有CoClassAttribute这样一个属性,CLR在做数据转换(Marshalling/Unmarshalling)的时候,知道这个接口是一个coclass interface,也就是一个直接对应一个COM对象的接口,并且可以轻松通过CoClassAttribute找到对应的MyComObjectClass这个强类型RCW。因此,CLR可以很容易根据这个信息,创建出一个新的强类型RCW的实例(当然也可能发现这个接口的值符合一个已有的RCW,并且直接返回之),也就是MyComObjectClass的实例。
情况#2下,ITest所对应的对象可能有好几种不同情况:
1. MyComObject这个COM对象
2. 另外的非托管COM对象,实现了ITest接口
3. 托管的CCW,实现了ITest接口
CLR是如何对这几种情况作出区分的呢?关键在于IManagedObject接口和IProvideClassInfo接口。
先谈IManagedObject接口。这个接口是.NET定义的,一旦某个对象实现了IManagedObject接口,说明此对象是托管对象,这也正是这个接口命名的由来。除此之外,IManagedObject在.NET Remoting中也起到了相当重要的作用,用于在服务器端获得序列化的缓冲区,然后在客户端反序列化得到原始对象的拷贝或者Proxy,因为与本文关系不大,因此这里从略。CLR在将非托管的接口指针转换成托管对象的时候,首先要做的就是做一个QueryInterface(IID_IManagedObject)调用,检查该COM对象是否是一个托管对象,如果是,则直接通过IManagedObject接口定义的GetObjectIdentity函数直接获得CCW的指针,返回之(这个CCW并非是原始托管对象,而是CLR的内部实现细节,要得到原始的托管对象,还需要做一系列的操作,这里从略)。顺便说一句,托管对象的CCW缺省实现了IManagedObject,并提供了IManagedObject接口中的函数实现,这个实现是所有CCW都共享的。同样的还有许多常用接口,如IUnknown,IMarshal,IConnectionPointContainer等等。
反之,如果这个COM对象没有实现IManagedObject接口,说明COM对象是非托管对象。这种情况下,我们必须要用到IProvideClassInfo接口。IProvideClassInfo,正如其名,是用来返回该COM对象所对应的信息的。IProvideClassInfo只有一个函数GetClassInfo,返回一个ITypeInfo指针。ITypeInfo也是一个COM接口,简单来说就是提供了COM对象的类型信息,类似.NET中的Type对象(但并不是Type对象)。通过ITypeInfo,可以拿到COM对象的CLSID,CLR然后根据这个CLSID来获得对应的托管类型。CLR会查找当前AppDomain中有那个类型是对应这个CLSID。如果没有找到,则回到注册表去查找CLSID所对应的COM注册表项(其实Manifest也可以,这是Registration-Free Com Interop)。一旦CLSID所对应的注册表中指定了对应托管类型和Assembly的名称,CLR便可以通过这个信息加载Assembly并找到对应的类型。这也正是Type.GetTypeFromCLSID所作的事情。一旦这个步骤成功,CLR便知道了这个接口所对应的COM对象的对应托管对象,从而可以通过这个接口指针创建一个强类型RCW。
当然了,这个步骤需要COM对象本身实现IProvideClassInfo接口。因此,当在设计一个非托管COM组件并希望这个COM组件能够比较容易的被.NET使用的话,推荐让这个非托管COM对象实现IProvideClassInfo接口。反之,如果这个COM对象没有实现这个接口(事实上很少COM对象实现这个接口),或者虽然COM对象实现了IProvideClassInfo接口,但是CLR无法通过CLSID成功找到对应的托管类型,CLR便别无选择,只能返回一个System.__ComObject作为弱类型的RCW。
本质上来讲,弱类型的RCW和强类型的RCW并没有太大的区别,只是强类型的RCW具有更多类型信息,比较容易调试。而在实际的编程中,使用方法是完全一致的,只是System.__ComObject必须要先转换到相应的接口,再调用。另外一个非常有趣的事情是,强类型RCW事实上是继承自System.__ComObject的,虽然从Metadata上面是无法看出来,但是在CLR内部这个继承关系确实存在。从本质上来讲,弱类型的RCW更接近非托管的COM编程方法(比如C++),因为用C++进行COM编程的时候,通常都是直接和接口打交道。而并不知道COM对象是什么。对于一个COM对象而言,使用接口来进行操作也是最符合COM本质的做法,我们也不希望在.NET中鼓励大家针对COM对象编程而不是针对COM接口编程。因此,CLR在将来有可能会渐渐减少对强类型RCW的支持,而转而建议使用弱类型RCW。当然,目前来讲由于工具的支持(主要是TlbImp),大部分情况下还是以强类型RCW为主。一旦大家遇到了弱类型的RCW,也无需惊慌,因为使用方法和强类型RCW并无太大区别。如果希望程序中不要出现弱类型的RCW,那么最好的方法还是使这个COM对象实现IProvideClassInfo接口。
作者: 张羿(ATField)
Blog: http://blog.csdn.net/atfield
http://blogs.msdn.com/yizhang
转载请注明出处