天天看点

Com编程入门——什么是COM,如何使用COM

本文的目的是为刚刚接触com的程序员提供编程指南,并帮助他们理解com的基本概念。内容包括com规范简介,重要的com术语以及如何重用现有的com组件。本文不包括如何编写自己的com对象和接口。

  com即组件对象模型,是component object model 取前三个字母的缩写,这三个字母在当今windows的世界中随处可见。随时涌现出来的大把大把的新技术都以com为基础。各种文档中也充斥着诸如com 对象、接口、服务器之类的术语。因此,对于一个程序员来说,不仅要掌握使用com的方法,而且还要彻底熟悉com的所有一切。

  本文由浅入深描述com的内在运行机制,教你如何使用第三方提供的com对象(以windows 外壳组件shell为例)。读完本文后,你就能掌握如何使用windows操作系统中内建的组件和第三方提供的com对象。

  本文假设你精通c++语言。在例子代码中使用了一点mfc和atl,如果你不熟悉mfc和atl也没关系,本文会对这些代码进行完全透彻的解释。本文包括以下几个部分:

com——到底是什么?——com标准的要点介绍,它被设计用来解决什么问题

基本元素的定义——com术语以及这些术语的含义

使用和处理com对象——如何创建、使用和销毁com对象

基本接口——描述iunknown基本接口及其方法

掌握串的处理——在com代码中如何处理串

应用com技术——例子代码,举例说明本文所讨论的所有概念

处理hresult——hresult类型描述,如何监测错误及成功代码

<a target="_blank">com—</a>

  简单地说,com是一种跨应用和语言共享二进制代码的方法。与c++不同,它提倡源代码重用。atl便是一个很好的例证。源码级重用虽然好,但只能用于c++。它还带来了名字冲突的可能性,更不用说不断拷贝重用代码而导致工程膨胀和臃肿。

  windows使用dlls在二进制级共享代码。这也是windows程序运行的关键——重用kernel32.dll, user32.dll等。但dlls是针对c接口而写的,它们只能被c或理解c调用规范的语言使用。由编程语言来负责实现共享代码,而不是由dlls本身。这样的话dlls的使用受到限制。

mfc引入了另外一种mfc扩展dlls二进制共享机制。但它的使用仍受限制——只能在mfc程序中使用。

  com通过定义二进制标准解决了这些问题,即com明确指出二进制模块(dlls和exes)必须被编译成与指定的结构匹配。这个标准也确切规定了在 内存中如何组织com对象。com定义的二进制标准还必须独立于任何编程语言(如c++中的命名修饰)。一旦满足了这些条件,就可以轻松地从任何编程语言 中存取这些模块。由编译器负责所产生的二进制代码与标准兼容。这样使后来的人就能更容易地使用这些二进制代码。

  在内存中,com对象的这种标准形式在c++虚函数中偶尔用到,所以这就是为什么许多com代码使用c++的原因。但是记住,编写模块所用的语言是无关的,因为结果二进制代码为所有语言可用。

  此外,com不是win32特有的。从理论上讲,它可以被移植到unix或其它操作系统。但是我好像还从来没有在windows以外的地方听说过com。

我们从下往上看。接口只不过是一组函数。这些函数被称为方法。接口名字以大写的i开头,例如c++中的ishelllink,接口被设计成一个抽象基类,其中只有纯粹的虚拟函数。

  接口可以从其它接口继承,这里所说的继承的原理就好像c++中的单继承。接口是不允许多继承的。

coclass(简称组件对象类——component object class)被包含在dll或exe中,并且包含着一个或者多个接口的代码。组件对象类(coclasss)实现这些接口。com对象在内存中表现为组件 对象类(coclasss)的一个实例。注意com“类”和c++“类”是不相同的,尽管常常com类实现的就是一个c++类。 

com服务器是包含了一个或多个coclass的二进制(dll或exe)。

注册(registration)是创建注册表入口的一个过程,告诉windows 操作系统com服务器放在什么位置。取消注册(unregistration)则相反——从注册表删除这些注册入口。

guid(谐音为“fluid”,意思是全球唯一标示符——globally unique identifier)是个128位的数字。它是一种独立于com编程语言的标示方法。每一个接口和coclass有一个guid。因为每一个guid都是全球唯一的,所以避免了名字冲突(只要你用com api创建它们)。有时你还会碰到另一个术语uuid(意思也是全球唯一标示符——universally unique identifier)。uuids和guids在实际使用时的用途是一样的。

类id或者clsid是命名coclass的guid。接口id或者iid是命名接口的guid。

在com中广泛地使用guid有两个理由:

guids只是简单的数字,任何编程语言都可以对之进行处理;

guids可以在任何机器上被任何人创建,一旦完成创建,它就是唯一的。因此,com开发人员可以创建自己特有的guids而不会与其它开发人员所创建的guids有冲突。这样就消除了集中授权发布guids的必要。

  hresult是com用来返回错误和成功代码的整型数字。除此之外,别无它意,虽然以h作前缀,但没有句柄之意。下文会对它有更多的讨论。

  最后,com库是在你使用com时与你交互的操作系统的一部分,它常常指的就是com本身。但是为了避免混淆才分开描述的。

<a target="_blank"> </a>

  每一种语言都有其自己处理对象的方式。

 例如,c++是在栈中创建对象,或者用new动态分配。因为com必须独立于语言,所以com库为自己提供对象管理例程。下面是对com对象管理和c++对象管理所做的一个比较:

创建一个新对象

c++中,用new操作符,或者在栈中创建对象。

com中,调用com库中的api。

删除对象

c++中,用delete操作符,或将栈对象踢出。

com中,所有的对象保持它们自己的引用计数。调用者必须通知对象什么时候用完这个对象。当引用计数为零时,com对象将自己从内存中释放。

  由此可见,对象处理的两个阶段:创建和销毁,缺一不可。当创建com对象时要通知com库使用哪一个接口。如果这个对象创建成功,com库返回所请求接口的指针。然后通过这个指针调用方法,就像使用常规c++对象指针一样。

创建com对象

为了创建com对象并从这个对象获得接口,必须调用com库的api函数,cocreateinstance()。其原型如下:

以下是参数解释:

   当你调用cocreateinstance()时,它负责在注册表中查找com服务器的位置,将服务器加载到内存,并创建你所请求的coclass实例。 以下是一个调用的例子,创建一个clsid_shelllink对象的实例并请求指向这个对象ishelllink接口指针。

   首先声明一个接受cocreateinstance()返回值的hresult和ishelllink指针。调用cocreateinstance()来 创建新的com对象。如果hr接受到一个表示成功的代码,则succeeded宏返回true,否则返回false。failed是一个与 succeeded对应的宏用来检查失败代码。

删除com对象

   前面说过,你不用释放com对象,只要告诉它们你已经用完对象。iunknown是每一个com对象必须实现的接口,它有一个方 法,release()。调用这个方法通知com对象你不再需要对象。一旦调用了这个方法之后,就不能再次使用这个接口,因为这个com对象可能从此就从 内存中消失了。

  如果你的应用程序使用许多不同的com对象,因此在用完某个接口后调用release()就显得非常重要。如果你不释放接口,这个com对象(包含代 码的dlls)将保留在内存中,这会增加不必要的开销。如果你的应用程序要长时间运行,就应该在应用程序处于空闲期间调用 cofreeunusedlibraries() api。这个api将卸载任何没有明显引用的com服务器,因此这也降低了应用程序使用的内存开销。

继续用上面的例子来说明如何使用release():

接下来将详细讨论iunknown接口。

  

每一个com接口都派生于iunknown。

这个名字有点误导人,其中没有未知(unknown)接口的意思。它的原意是如果有一个指向某com对象的iunknown指针,就不用知道潜在的对象是什么,因为每个com对象都实现iunknown。

iunknown 有三个方法:

addref() —— 通知com对象增加它的引用计数。如果你进行了一次接口指针的拷贝,就必须调用一次这个方法,并且原始的值和拷贝的值两者都要用到。在本文的例子中没有用到addref()方法;

release() —— 通知com对象减少它的引用计数。参见前面的release()示例代码段;

queryinterface() —— 从com对象请求一个接口指针。当coclass实现一个以上的接口时,就要用到这个方法;

   前面已经看到了release()的使用,但如何使用queryinterface()呢?当你用cocreateinstance()创建对象的时候,你得到一个返回的接口指针。如果这个com对象实现一个以上的接口(不包括iunknown),你就必须用queryinterface()方法来获 得任何你需要的附加的接口指针。queryinterface()的原型如下:

  让我们继续外壳链接的例子。它实现了ishelllink 和ipersistfile接口。如果你已经有一个ishelllink指针,pisl,可以从com对象请求ipersistfile接口:

  然后使用succeeded宏检查hr的值以确定queryinterface()的调用情况,如果成功的话你就可以象使用其它接口指针那样使用新的接口指针,pipf。但必须记住调用pipf-&gt;release()通知com对象已经用完这个接口。

这一部分将花点时间来讨论如何在com代码中处理串。如果你熟悉unicode 和ansi,并知道如何对它们进行转换的话,你就可以跳过这一部分,否则还是读一下这一部分的内容。

   不管什么时候,只要com方法返回一个串,这个串都是unicode串(这里指的是写入com规范的所有方法)。unicode是一种字符编码集,类似ascii,但用两个字节表示一个字符。如果你想更好地控制或操作串的话,应该将它转换成tchar类型串。

  tchar和以_t开头的函数(如_tcscpy())被设计用来让你用相同的源代码处理unicode和ansi串。在大多数情况下编写的代码都是 用来处理ansi串和ansi windowsapis,所以在下文中,除非另外说明,我所说的字符/串都是指tchar类型。你应该熟练掌握tchar类型,尤其是当你阅读其他人写的 有关代码时,要特别注意tchar类型。

  当你从某个com方法返回得到一个unicode串时,可以用下列几种方法之一将它转换成char类型串:

调用 widechartomultibyte() api;

调用crt 函数wcstombs();

使用cstring 构造器或赋值操作(仅用于mfc );

使用atl 串转换宏;

你可以用widechartomultibyte()将一个unicode串转换成一个ansi串。此函数的原型如下:

codepage:unicode字符转换成的代码页。你可以传递 cp_acp来使用当前的ansi代码页。代码页是256个字符集。字符0——127与ansi编码一样。字符128——255与ansi字符不同,它可

以包含图形字符或者读音符号。每一种语言或地区都有其自己的代码页,所以使用正确的代码页对于正确地显示重音字符很重要。

dwflags:dwflags 确定windows如何处理“复合” unicode字符,它是一种后面带读音符号的字符。

如è就是一个复合字符。如果这些字符在codepage参数指定的代码页中,不会出什么事。

否则,windows必须对之进行转换。 传递wc_compositecheck使得这个api检查非映射复合字符。

传递wc_sepchars使得windows将字符分为两段,即字符加读音,如e`。

传递wc_discardns使得windows丢弃读音符号。

传递wc_defaultchar使得windows用lpdefaultchar参数中说明的缺省字符替代复合字符。

缺省行为是wc_sepchars。

lpwidecharstr 要转换的unicode串。

cchwidechar lpwidecharstr在unicode 字符中的长度。通常传递-1,表示这个串是以0x00结尾。

lpmultibytestr 接受转换的串的字符缓冲 cbmultibyte lpmultibytestr的字节大小。

lpdefaultchar 可选——当dwflags包含wc_compositecheck | wc_defaultchar并且某个unicode字符不能被映射到同等的ansi串时所传递的一个单字符ansi串,包含被插入的“缺省”字符。可以

传递null,让api使用系统缺省字符(一种写法是一个问号)。

lpuseddefaultchar 可选——指向bool类型的一个指针,设置它来表示是否缺省字符曾被插入ansi串。可以传递null来忽略这个参数。

  我自己都有点晕菜了……!,万事开头难啊……,不搞清楚这些东西就很难搞清楚com的串处理。何况文档中列出的比实际应用的要复杂得 多。下面就给出了如何使用这个api的例子:

调用这个函数后,szansistring将包含unicode串的ansi版本。 调用这个函数后,szansistring将包含unicode串的ansi版本。

这个crt函数wcstombs()是个简化版,但它终结了widechartomultibyte()的调用,所以最终结果是一样的。其原型如下:

  wcstombs()在它对widechartomultibyte()的调用中使用wc_compositecheck | wc_sepchars标志。用wcstombs()转换前面例子中的unicode串,结果一样:

mfc中的cstring包含有构造函数和接受unicode串的赋值操作,所以你可以用cstring来实现转换。例如:

  atl有一组很方便的宏用于串的转换。w2a()用于将unicode串转换为ansi串(记忆方法是“wide to ansi”——宽字符到ansi)。实际上使用ole2a()更精确,“ole”表示的意思是com串或者ole串。下面是使用这些宏的例子:

  ole2a()宏“返回”转换的串的指针,但转换的串被存储在某个临时栈变量中,所以要用lstrcpy()来获得自己的拷贝。其它的几个宏是w2t()(unicode 到 tchar)以及w2ct()(unicode到常量tchar串)。

  有个宏是ole2ca()(unicode到常量char串),可以被用到上面的例子中,ole2ca()实际上是个更正宏,因为lstrcpy()的第二个参数是一个常量char*,关于这个问题本文将在以后作详细讨论。

  另一方面,如果你不想做以上复杂的串处理,尽管让它还保持为unicode串,如果编写的是控制台应用程序,输出/显示unicode串时应该用全程变量std::wcout,如:

  但是要记住,std::wcout只认unicode,所以你要是“正常”串的话,还得用std::cout输出/显示。对于unicode串文字量,要使用前缀l标示,如:

如果保持串为unicode,编程时有两个限制:

必须使用wcsxxx() unicode串处理函数,如wcslen();

在windows 9x环境中不能在windows api中传递unicode串。要想编写能在9x和nt上都能运行的应用,必须使用tchar类型,详情请参 考msdn;

下面用两个例子演示本文所讲的com概念。

代码中还包含了本文的例子工程。

使用单接口com对象

   第一个例子展示的是单接口com对象。这可能是你碰到得最简单的例子。它使用外壳中的活动桌面组件对象类(clsid_activedesktop)来获得当前桌面墙纸的文件名。请确认系统中安装了活动桌面(active desktop)。 以下是编程步骤:

初始化com库。 (initialize);

创建一个与活动桌面交互的com对象,并取得iactivedesktop接口;

调用com对象的getwallpaper()方法;

如果getwallpaper()成功,则输出/显示墙纸文件名;

释放接口(release());

收回com库(uninitialize);

   在这个例子中,输出/显示unicode 串 wszwallpaper用的是std::wcout。

使用多接口的com对象

   第二个例子展示了如何使用一个提供单接口的com对象queryinterface()函数。其中的代码用外壳的shell link组件对象类创建我们在第一个例子中获得的墙纸文件的快捷方式 。以下是编程步骤:

初始化 com 库;

创建一个用于建立快捷方式的com 对象并取得ishelllink 接口;

调用ishelllink 接口的setpath()方法;

调用对象的queryinterface()函数并取得ipersistfile接口;

调用ipersistfile 接口的save()方法;

释放接口;

收回com库;

   这一部分准备用succeeded 和

failed宏进行一些简单的出错处理。主要是深入研究从com方法返回的hresult,以便达到完全理解和熟练应用。

  hresult是个32位符号整数,其非负值表示成功,负值表示失败。hresult有三个域:程度位(表示成功或失败),功能码和状态码。功能码表 示hresult来自什么组件或程序。微软给不同的组件多赋予功能码,如:com、任务调度程序等都有功能码。功能码是个16位的值,仅此而已,没有其它 内在含义;它在数字和意义之间是随意关联的;类似getlasterror()返回的值。

  如果你在winerror.h头文件中查找错误代码,会看到许多按照[功能]_[程度]_[描述]命名规范列出的hresult值,由组件返回的通用的hresult(类似e_outofmemory)在名字中没有功能码。如 :

regdb_e_readregdb:

功能码 = regdb, 指“注册表数据库(registry database)”;

程度 = e 意思是错误(error);

描述 = readregdb 是对错误的描述(意思是不能读注册表数据库)。 s_ok: 没有功能码——通用(generic)

hresult;

程度=s;表示成功(success);

ok 是状态描述表示一切都好(everything''s ok)。

   好在有一种比察看winerror.h文件更容易的方法来确定hresult的意思。使用vc提供的错误查找工具(error lookup)可以轻松查到为hresult内建功能码。例如,假设你在cocreateinstance()之前忘了调用 coinitialize()。cocreateinstance()返回的值是0x800401f0。你只要将这个值输入到错误查找工具按“look up”按钮,便可以看到错误信息描述“尚未调用coinitialize”如下图所示:

Com编程入门——什么是COM,如何使用COM

  另外一种查找hresult描述的方法是在调试器中。假设有一个hresult变量是hres。在watch窗口的左边框中输入“hres,hr”,表示想要看的值,“hr”便会通知vc显示hresult所描述的值。如下图所示:

Com编程入门——什么是COM,如何使用COM

通过以上的讨论,想必你对com编程有了初步的认识,本文第二部分将探讨com的内部机制。教你如何用c++编写自己的接口。