用C#做WinForm程序,时间长了难免会遇到和COM组件打交道的地方,用什么方式创建COM对象也成了我们必须面对的一个问题.据我所知道的创建COM对象的方法一共有以下几种:
1 使用.NET包装COM组件
这是最简单的就是导入COM组件所在的DLL,让IDE生成.NET一个IL包装加到项目中,这样原来COM里面所有实现了IDispatch,Dual的COM类型及其相关类型就可以直接在.NET程序里面使用,比如以前在2003时代,想要写自己的基于IE的浏览器,就得手动加入与IWebBrowser2接口相关的DLL,这种方式是大家最常用的,也是最傻瓜化的,因此也没什么可解释的.
但是这种方式有个至命的缺点---不是所有的COM对象都能用这种方式导出.正如前面所说的,只有实现了IDispatch,Dual类型的接口才支持被导出,而且面对不同版本的COM或许会生成不一样的导出DLL,比如说A机器上写代码时导入了一个Jet2.6版本的包装DLL,代码编译了拿到B机器上去运行,但是B机器上的Jet版本是2.8的,就可能会出现运行时错误.
2 用反射动态创建
包括使用Type.GetTypeFromCLSID和Type.GetFromProgID两种方法获取COM对象的Type再创建.这种方式也好理 解,就是说使用这两个方法之前,必须得知道COM对象的GUID或ProgID,好在这也不是什么难事,一般我们要使一个COM对象,多多少少都了解一些 这个COM对象的GUID或ProgID信息.用这种方获取到了一个Type对象后,就可以用.NET里面通用的反射创建对象的方法来做了.
这里给出一个创建JetEngine 的COM对象的代码实例:
2 {
3 Type t = Type.GetTypeFromCLSID(clsid);
4 if (t == null ) return null ;
5
6 return Activator.CreateInstance(t);
7 }
8
9 Guid g = new Guid( " DE88C160-FF2C-11D1-BB6F-00C04FAE22DA " ); // JetEngine
10 object jet = GetActiveXObject(g);
是不是觉得最后调用GetActiveXObject(g)的地方和IE里面Javascript里面用new ActiveXOjbect创建COM对象的方法很相像?
3 声明CoCreateInstance外部函数,用这个函数去创建相应的COM实例
M$在2005里面包装的WebBrowser控件内部就是用这个函数去创建的, 使用这种方式创建COM,就跟在C++里面不什么两样了.有一点需要说明的是,一般我们在代码中引入外部方法的时候,方法的参数和返回值的类型不一定是唯 一的一种,只要在逻辑上相互能转化,一般都可以使用.
比如说如下几种声明都是正确的:
2 [DllImport( " ole32.dll " , ExactSpelling = true , PreserveSig = false )]
3 public static extern object CoCreateInstance([In] ref Guid clsid,
4 [MarshalAs(UnmanagedType.Interface)] object punkOuter, int context, [In] ref Guid iid);
5
6 [DllImport( " ole32.dll " , ExactSpelling = true , PreserveSig = false )]
7 public static extern IntPtr CoCreateInstance([In] ref Guid clsid,
8 IntPtr punkOuter, int context, [In] ref Guid iid);
9
10 [DllImport( " ole32.dll " , ExactSpelling = true )]
11 public static extern int CoCreateInstance([In] ref Guid clsid,
12 IntPtr punkOuter, int context, [In] ref Guid iid, [Out] out IntPtr pVoid);
13
14 [DllImport( " ole32.dll " , ExactSpelling = true )]
15 public static extern int CoCreateInstance([In] ref Guid clsid,
16 [MarshalAs(UnmanagedType.Interface)] object punkOuter, int context,
17 [In] ref Guid iid, [MarshalAs(UnmanagedType.Interface), Out] out object pVoid);
甚至于当你有里面对应的接口类型的声明的时候,完全可以把上面的object或IntPtr换成相应的接口类型,前提是你的接口类型的声明一定要 正确.读者中用C++做过COM的一定对这种方式记忆犹新吧,只不过这里不再需要什么CoInitialize和CoUninitialize,.NET 内部自己帮你搞定了.顺便提一下,上面例子中的object与IntPtr声明是相通的,我们可以用Marshal.GetObjectForIUnknown和Marshal.GetIUnknownForObject这两个方法在object和IntPtr之间互转,前题当然是这两种方式所指向的都是COM对象才行.这种方式提供的传入参数最多,创建对象也最灵活.
3.直接声明空成员的类
可能很多程序员对于这个不太理解这是什么意思,没关系咱还是"用代码来说话".
2 public class JetEngineClass
3 {
4 }
5
6 [ComImport, CoClass( typeof (JetEngineClass)), Guid( " 9F63D980-FF25-11D1-BB6F-00C04FAE22DA " )]
7 public interface IJetEngine
8 {
9 void CompactDatabase(
10 [In, MarshalAs(UnmanagedType.BStr)] string SourceConnection,
11 [In, MarshalAs(UnmanagedType.BStr)] string Destconnection
12 );
13 void RefreshCache([In, MarshalAs(UnmanagedType.Interface)] object Connection);
14 }
15
16 JetEngineClass engine = new JetEngineClass();
17 IJetEngine iengine = engine as IJetEngine;
18 // iengine即是所要用的接口的引用
大家看到了上面声明的JetEngineClass类只有一个单单的类声明,但是没有一个成员声明,但是和一般的类声明有些不一样的是这个类多了 两个特性(Attribute),把这个类和COM对象联系在一起的就是这两个特性了,其中一个是ComImportAttribute,这个特性指明了 所作用的类是从COM对象中来的,GuidAttribute指明了COM对象的GUID,也就是说明了创建这个COM需用到的GUID。有了这两个特性 以后,这个类就不是一个普通的类了,当我们使用new去创建实例的时候,CLR看到了声明的这两特性就知道要创建的是一个COM对象,根据提供的GUID 也就能创建出指定的COM对象,并和new返回的对象实例关联在一起了。
终上4种方法我们可以看出来,第一种方式只对特定的COM对象有效,不具有通用性;第二种方式只需要知道COM对象的CLSID或PROGID就可以了, 是我们在.NET里平时比较常用的创建COM对象的方法;第三种方式需要自己声明一个外部方法,而且需要传入若干的参数,还需要知道COM对象模型,是单 线程呢还是多线程,进程内呢还是进程外,两个字"麻烦"。对CoCreateInstance这个方法不是很熟悉的人来说,用起来就不那么顺手了;第四种 方式用起来最像是.NET的方式,也最简单省事,和其它.NET对象的创建方式最为接近。四种方法各有各有好处,我觉得简单的COM对象,用第二种和第四 种是最好的(我个人来说最喜欢第四种)又不生成额外的程序集;要是COM对象相关的比较多,比如说Excel之类的COM对象,我建议还是用导入类型库包 装吧,虽然是有可能出现版本问题,但这种应该很容易要求目标机器上运行的COM版和开发的时候一致的,更何况版本问题也不是100%出现,只是很少一部分 会出这样的问题。最不推荐的就是第三种方式了,这种方式在我看来唯一用到的地方就是使用IntPtr作为COM对象和接口的指针的时候,或者是想要在创建 COM对象的时候,对参数作最灵活的控制的时候. 因为其它三种方式既不能返回IntPtr指针(其实也可以通过前面提到的的Marshal类的方法把.NET包装的COM对象转成指针),也不能提供与直 接调用CoCreateInstance函数提供最全面的参数相匹配的方式。
最后提个小问题
1 读者有兴趣的话可以去看看这几种方式(不包括第三种)生成的COM对象的引用的类型是否是一致的,也就是用GetType得到的Type是否是一致的
2 大家猜猜这段代码运行后,iengine的类型会是什么(GetType的结果), 会和engine的类型一样吗?
结论就是t1,t2,t3是三个不同的引用,也就是说在.NET里面代表了三种不同的类型,但是三种类型的GUID却是一样的,因为在COM里 GUID代表了一个COM类,只要GUID是一样的那么就表示是一个COM类,因此仅从COM类这一角度出发的话,这三种类型就是同一个COM类型。
第1种方式创建的COM对象的.NET包装的类型一般来说就是COM导入的.NET包装程序集里面对应声明的类型.
第2种方式创建的COM对象的.NET包装的类型永远都是__ComObject.
第3种方式创建的COM对象的.NET包装或者是指针经过Marshal类的方法转成的.NET的包装,这两种方式对应的类型__ComObject.
第4种从本质上来讲是第1种方式的变种,只是更为灵活,使用范围更加广范了,因此对应的类型也应该是声明的时候的.NET中的类型
上一文里面留的第二个问题的结果就是原来是什么类型,经过一次Marshal类的方法与IntPtr互转换后的结果还是什么类型,应该是CLR内部 记录了指针和.NET类型之前的对应关系,不会每次由IntPtr转到object的时候都用一个不同的包装(感觉有点像WinForm里面从 Handle找Control一样).
上一篇我们讲到了C#中创建COM对象的几种方式。不知大家也注意到了,最后一种方式中JetEngineClass类并没有提供方法供我们调用,要使用 它的话必须先把这个引用转成接口引用才能直接使用里面的方法,实现早期函数绑定。虽然我们在声明JetEngineClass类的时候并没指定该类实现了 IJetEngine接口,但是后面在使用的时候却直接把engine用as操作转成了IJetEngine接口,而且居然转成功了。而且大家也可以用 is操作符测试一下,engine is IJetEngine反回的结果也为true。这就是本篇要讲的---C#中COM对象接口的查询。
与COM创建的方法一样,C#中COM接口查询的方法也有好几种:
第1种 Marshal.QueryInterface方法
这个方法本身就是Framework提供的正统的用来查询COM对象的方法,这种方式MSDN上已经有详细的说明了,我也不再多说.唯一注意的是这里只能 传COM对象的指针IntPtr,而且这个方法成功返回后,所引用的COM对象的计数会自增1.以后当返回的查询到的接口的指针不再使用了的时候,需要手 动调用Marshal.Release,达到平衡COM引用计数的目的.虽说是简单,还是给段代码吧
2
3 IntPtr pJet;
4 Guid g = typeof (IJetEngine).Guid;
5 int hr = Marshal.QueryInterface(pJetClass, ref g, out pJet);
6 if (hr < 0 )
7 Marshal.ThrowExceptionFromHR(hr);
8
其实在使用IntPtr引用COM对象的时候,就像是在C++里面直接使用COM指针一样,理论上来说这个指针每复制一次,都需要我们手动的调用一 次AddRef方法,增加COM对象的引用计数,每当我们把指针设置为无效或不再使用这个指针的时候,同样需要手动的把这个指针用Release方法减少 引用计数,当引用计数变为0的时候就释放COM对象.这还是没有摆脱C++里面使用原始的COM指针的时候容易忘记平衡引用计数的问题.这里我故意使用 了"原始的COM指针"这外概念,主要是区别于在C++里面我们常使用COM指针的另外一种方式COMPtr<T>泛型类,有了这个泛型类 C++里面的COM对象的引用计数就能够正常及时的增加和减少了,使得开发人员不用花心思在COM引用计数的维护上.但是就算是这样,要想查询一个接口还 是摆脱不了那个QueryInterface方法.
C#作为一种继承了C++大部分优点的一种语言,当然也提供了类似的方式让我们远离引用计数的陷阱,而且还提供了更加优雅的方式供我们使用.
这就是我们要讲的第2种COM接口查询的方式
第2种 与C#语言一致的类型转换方式
大家知道在C#里面我们要想把一种类型的引用转成另外一种类型有两种方式,第一种类似于(IJetEngine)engine这样;第二种类似于 engine as IJetEngine这样.这两种方式有的时候产生的效果是一样的,但是严格说来还是有很多差别的,这个在学C#的时候大家都遇到了,这里我也不在多说, 只是提几个下面会用到的相同点和区别.
对于都是引用类型的转换,大家都不产生新的对象,如果转换成功的话都是返回指向给定对象的新的类型的引用.第一种强制类型转换(暂且称作这样吧),在遇到 转换不成功的时候会抛出异常,但是大多数时候我们都不希望抛出异常,而是希望当转换不成功的时候,返回null引用就可以了,而这正是第二种方式'as' 方式所能够达到的.
这两种类型转换方式同样可以作用在COM对象的C#包装的引用上,而产生的效果与前面用QueryInterface产生的效果是一样的,都是返回一个给 定的接口,只不过这里以具体的接口声明的引用代替了之前的接口指针.而且这种转换方式与一个普通的C#托管类转换到实现的接口的方式简直是一模一样.代码 风格的一致性也得到了更好的体现.
需要注意的是我们用这种方式用COM对象的类型转换(其实是接口查询)的时候,还是与普通的拖管类的类型转换有一些细微的差别,但不是体现在代码上,而是体现在转换前后的两个类型的关系上:
2 {
3 }
4 public class Demo : IDemo
5 {
6 }
7
8 public class Demo1
9 {
10 }
11
12 object o1 = new Demo();
13 object o2 = new Demo();
14 IDemo d1 = o1 as IDemo; // d1获得了一个IDemo的引用
15 IDemo d2 = o2 as IDemo; // d2 值为 null
16
17 IJetEngine e = new JetEngineClass() as IJetEngine; // e获得了一个IJetEngine的引用
从这里我们可以看到普通托管类如果声明的时候没有实现某个接口,那么在类型转换的时候,一定不会转成功,但是一旦某个托管类声明成了COM类的包装 类以后,不管在声明的时候有没有实现相应的接口,只要所指代的COM类用QueryInterface能够找到这个接口,甚至是一个聚合的接口,那么这里 的转换一定成功.在这里类型转换的功能就好像就成了QueryInterface的功能了. 同样的C#里面与as操作符是孪生兄弟的"is"操作符在这里也不在是面向对象里面的"is a...", "has a ..."的定义,变成了QueryInterface能不能成功的标志了.
第3种 声明的接口从IUnknown接口派生,或包含IUnknown接口的三个方法,我们还是来看看具体的代码:
2
3 [ComImport, Guid( " 00000000-0000-0000-C000-000000000046 " )]
4 public interface IUnknown
5 {
6 void QueryInterface([In] ref Guid iid, [Out] out IntPtr ppvObj);
7 int AddRef();
8 int Release();
9}
10
11 [ComImport, CoClass( typeof (JetEngineClass)), Guid( " 9F63D980-FF25-11D1-BB6F-00C04FAE22DA " )]
12 public interface IJetEngine1 : IUnknown
13 {
14 void CompactDatabase(
15 [In, MarshalAs(UnmanagedType.BStr)] string SourceConnection,
16 [In, MarshalAs(UnmanagedType.BStr)] string Destconnection
17 );
18 void RefreshCache([In, MarshalAs(UnmanagedType.Interface)] object Connection);
19}
20
21 [ComImport, CoClass( typeof (JetEngineClass)), Guid( " 9F63D980-FF25-11D1-BB6F-00C04FAE22DA " )]
22 public interface IJetEngine2
23 {
24 void QueryInterface([In] ref Guid iid, [Out] out IntPtr ppvObj);
25 int AddRef();
26 int Release();
27
28 void CompactDatabase(
29 [In, MarshalAs(UnmanagedType.BStr)] string SourceConnection,
30 [In, MarshalAs(UnmanagedType.BStr)] string Destconnection
31 );
32 void RefreshCache([In, MarshalAs(UnmanagedType.Interface)] object Connection);
33}
34
35 IJetEngine1 iJetEngine = GetJetEngine() as IJetEngine1;
36 IntPtr p1;
37 iJetEngine.QueryInterface( typeof (IUnknown).Guid, out p1);
38
39 IJetEngine2 iJetEngine = GetJetEngine() as IJetEngine2;
40 IntPtr p2;
41 iJetEngine.QueryInterface( typeof (IUnknown).Guid, out p2);
上面两种方式都是正确的,需要注意的是如果把IUnknown的方法放到IJetEngine2接口内部声明的话,必须放到函数声明的最开始位置,想想虚 函数编译后函数指针的顺序就明白了.不过这种方式有个不太好的地方就是搞了老半天好不容易才得到的一个对COM对象的包装类,经过这么一查询接口,又回到 了指针形态,很是不爽.
这里说了几种COM接口查询的方式,无非就是COM对象的.NET包装类的引用或者IntPtr指针转来转去的,这两种COM对象的引用到底哪种更好点 呢.我的建议是能用包装类引用的尽量用包装类引用吧,实在不济的时候没有声明包装类也可以用object作为引用类型.
我是不太喜欢直接操作COM对象的IntPtr指针的(非它类型的IntPtr指针除外,例如一个指向内存数据块的指针),除非是实在没有办法的时候.原 因嘛,就是因为COM引用计数器的问题.前面我们也提到过了,使用COM包装类的引用的时候,不管在接口之间怎么转换,都不会产生新的对象;还有一点就是 COM对象的引用计数只会在生成包装类的实例的时候才会增加1;另外COM包装类也是一个托管类,只不过是一个比较特殊的托管类而以,所以它的实例的生命 周期还是遵循了一般托管类的生命周期的定义----当该对象没有被任何一个变量所引用的时候,这个对象就需要被垃圾回收了.结合以上几条,一个COM对象 只被包装类的实例引用时,在整个包装类的生命周期内,COM的引用计数都只是1,直到包装类被垃圾回收了,这个时候CLR会自动减少这个包装类所指向的 COM对象的引用计数,当计数器为0时COM对象也就被销毁了.这个比C++里面的ComPtr还要妙,ComPtr在每赋值一次的时候还要对引用计数加 1呢.
回过头来我们再看看使用IntPtr的情况,正如前面所说的,理论上来讲每赋值一次IntPtr都需要对COM计数加1,每当一个有效的IntPtr不再 使用了又要对其所引用的COM对象的计数器减1,对于现在C#程序员来说,很多甚至对内存的动态分配和释放都没有概念,更是会经常还要忘了COM计数器的 这些操作,编程的乐趣就这样被消磨得没有了,何其痛苦呀.
另外就是在使用自己定义COM包装类和接口的时候,经常会遇到一个接口的方法里面用到了另外的接口,如果一层一层展开下去会需要声明一大堆的接口定义,而 我们其实中是需要其中的一个很少的功能,这样太得不尝失了.最简单的方法就从我们的需要出发,保留我们需要调用的方法的接口的声明,其它不相干的接口的参 数用object类型或IntPtr定义,在用object作为参数类型的时候需要在参数上加上 MarshalAs(UnmanagedType.Interface)特性,以表明这是一个COM接口,而不是一个其它什么类型,例如结构什么的.