《什么是com编程--vc++版本.pdf》由会员分享,可在线阅读,更多相关《什么是com编程--vc++版本.pdf(22页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、 COM 漫谈vc+下什么是下什么是 com 编程编程 一:概述 PC 机自从诞生以来,硬件经历了无数变化,CPU 从最初的 INTEL 8086 到 现在 PIII 满大街都是也只不过十几年。微软的 WINDOWS 操作系统从最初的 1.0 版本到现在即将推出 WIN2000,一直是桌面系统上装机量最大的 OS。作为软件开发人员,使用着包括 Visual Basic,Visual C+,Delphi 包 括最新的 Borland C+builder 等等在内的众多开发环境为 WINDOWS 开发 应用程序。应该说现在的开发条件和若干年以前比已经是大大的进步了。如果你开发过 16 位的 WIN
2、DOWS 程序,你可能知道为了读取一个文件,我 们不得不使用一小段汇编来调用 DOS 例程,或者使用当时 WINDOWS 尚未公开 的函数:_lopen()。在 win32 环境下,你所要做的全部是调用 :CreateFile()来获得一个文件句柄,当然如果使用 MFC 或是 OWL 之类的 东西,你可以更简单的做到。不过一般情况下,程序员仍然不得不从头 开始写编写应用程序的每一行代码。但这种情况得到了改变:微软提出了 C O M(Component Object Model,中文也可以译作组件对象模型)概念,并且在最新的 WINDOWS95/98 以 及 WIN NT4 中越来越广泛的使用它
3、:我们有理由相信在不久的将来,C O M 将成为构建应用程序最普遍的方法,如果你对此技术有兴趣,不妨参考本 文,希望从中你能学到想知道的知识。如果你已经是 C O M 老手,也欢迎 你批评指正 本文是针对 C+程序员写的。在介绍概念的时候,我尽量不把 WIN32 API 的知识混合进来,以便你能够更清晰的看到 C O M 的本质。所有的例子都 用 Microsoft Visual C+5(SP3)编译通过。一般的讲,一个应用程序总是由单个的二进制文件组成。在以前,如果 这个程序需要做一些改进,就要修改源代码,然后编译,声称新的文件,然后取代原来的文件。现在,我们用一种全新的角度来看问题:把原先
4、一 整个的 EXE 可执行文件,分割成功能不同,但相对独立的几个部分,把他 们拼装起来,组成程序,组成软件。在未来程序发布以后,如果意识到需 要对他进行修改,只要替换有问题的或是需要升级的组建就可以了。甚至 可以做到再不影响程序正常运行的情况下替换其中的部件。如果你熟悉 WINDOWS 编程,可能会想到:DLL 似乎就是你所说的东西:可以动态下,动 态连接。事实上,COM 正是充分利用了 Win32 DLL 的灵活性才得以真正在 Windows 平台上实现的。这样做有哪些优点呢?首先:用户一般希望能够定制所用的应用程序,而组件技术从本质上讲就是可被定制的,因而用户可以用更能满足他们需 要的某个
5、组件来替换原来的那个。其次,由于组件是相对应用程序独立的 部件,我们可以在不同的程序中使用同一个组件而不会产生任何问题,软 件的可重用性将大大的得到增强。第三,随着网络带宽及其重要性的提高,分布式网络应用程序毫无疑问的成为软件市场上越来越重要的买点。组件 价构可以使得开发这类应用程序的过程得以简化。那么,COM 到底是什么呢?它是一个说明如何建立可动态互变组件的规范。他定义了一些为保证能互操作,客户(一个术语,指需要某种组件的程序)组件必须遵循的标准,COM 规范就是一套为组件架构设置标准的文档形 式的规范。COM 的发布形式是:以 win32 动态链接库(DLL)或者可执行文件 (EXE)的
6、形式发布的可执行代码组成。COM 组件是动态连接的,而且 COM 组件是完全与语言无关的。同时,COM 组 件可以以二进制的形式发布。COM 组件还可以在不妨碍老客户的情况下被升 级成新的版本。你现在可以认为,COM 所能提供的服务有些类似 C+中的类。不过类是基 于源代码的,COM 则不是。不过这里要澄清一些关于 COM 的错误观点:首先,COM 不是一种计算机语言。把 COM 同某种计算机语言(如 C+,VB)相比较是 没有意义的。其次,也不要把 DLL 和 COM 做比较,因为 COM 技术正是利用了 DLL 的动态链接能力才得以实现的,而现在一般观点则认为,利用 DLL 动态 链接能力
7、最佳的方法是 COM。当然,COM 也不是 win32 API 那样的一个函数集:它并没有支持或者提供类 似 MoveWindow 这样的函数来对系统进行特定的操作。COM 也并不是类似于 MFC 那样的 C+类库。COM 给开发人员提供的是一种开发与语言无关的组件库的 方法,但 COM 本身并没有提供任何实现。在一定程度上可以认为 COM 是系统 无关的,software AG 组织正在开发一系列 COM 支持系统,有望在不久的将 来,包括从 Mac OS,VMS,SCO UNIX 到 LINUX 的操作系统上都将得以实现 COM。COM 的确有一些具体的实现。COM 本身要实现一个称为 C
8、OM 库(COM library)的 API,它提供诸如客户对组件的查询,以及组件的注册/反注册等一系列 服务,一般来说,COM 库由操作系统加以实现,程序员不必关心其实现的细 节。总体来看,COM 提供了编写组件的一个标准方法。遵循 COM 标准的组件可 以被组合起来以形成应用程序。至于这些组件是谁编写的,是如何实现的 并不重要。组件和客户之间通过接口来发生联系。二:什么是接口 前面已经提到过,COM 组件与客户大家打交道的唯一办法是通过接口。在 C+的实现中,我们一般用抽象基类来定义接口,然后利用 C+类的多重继 承实现该组件。下面给出一个简单的示意:/iface.h /#ifndef I
9、FACE_H#define IFACE_H 1#define interface class interface IA public:virtual func1()=0;virtual func2()=0;interface IB public:virtual func3()=0;virtual func4()=0;#endif /-iface.h end-/test.c /#include iface.h class Ca:public IA,IB public:Ca(int i):m_Count(i)virtual func1()cout IA:func1 is m_Count*1 end
10、l;virtual func2()cout IA:func2 is m_Count*2 endl;virtual func3()cout IB:func3 is m_Count*3 endl;virtual func4()cout IB:func4 is m_Count*4 func1();pIa-func2();pIb-func3();pIb-func4();delete pCa;/-test.c end-/上例中,定义了 IA,IB 两个接口,你可以注意到他们所有的成员函数都 被声明为 virtual,并且在函数末尾用=0 做了结束。类似这样的函数 我们在 C+中称之为纯虚函数,如果整个的
11、类都由纯虚函数组成,那么 这个类就叫做抽象基类。抽象基类本身由于没有实体函数与变量,所以 并不分配内存。一般它的用途是为派生类指定内存结构。打个比方来说,就好像把房子分割成很多小间,规定以后哪些小间应该放什么(函数的 实体)但具体的东西则要等派生类来填放。这里有一个概念需要说明一下:组件并不是类,上面我们用一个类就实 现了两组接口,同样我们也可以用它来实现更多接口。组件本身其实只 是一个接口集及其实现的集合。一个组件可能包含了多个接口,每一个 接口都有各自的实现。同时,接口并非总是继承的,COM 规范没有要求实现某个接口的类必 须从那个接口继承。这是因为客户并不了解 COM 组件的继承关系。对
12、接口 的继承只不过是一种实现细节而已。下面将介绍 QueryInterface 函数。这个函数被用来查询其他接口。客户 于组件之间的通讯是通过接口完成的。哪怕是客户查询其他一个组件时,也需要通过一个接口(换而言之,如果一个组件不支持这个接口,那他 一定不是一个 COM 组件)这个接口的名字叫 IUnknown,它有三个函数,如 下所示:interface IUnknown virtual HRESULT _stdcall QueryInterface(const IID&iid,void*ppv)=0;virtual ULONG _stdcall AddRef()=0;virtual ULON
13、G _stdcall Release()=0;COM 组件的所有接口都继承了 IUnknown,这样一来,每一个接口的前三个函数都 是 QueryInterface,这就是的所有的 COM 接口都可以被当成是 IUnknown 来处理。客户只要通过一个 CoCreateInstance 函数就可以创建该组件的实例并且获取其 IUnknown*。HRESULT _stdcall CoCreateInstance(const CLSID&clsid,IUnknown*pIUnknownOuter,DWORD dwClsContext,const IID&iid,void*ppv );下面的 COD
14、E 演示创建一个组件:extern C const GUID CLSID_COM1=(0 x32bb8230,0 xb41b1 0 x11cf,0 xa6,0 xbb,0 x0,0 x80,0 xc7,0 xb2,0 xd6,0 x82);extern C const GUID IID_IX=(0 x32bb8230,0 xb591c 0 x11ff,0 xc1,0 xb0,0 xc7,0 xf8,0 x21,0 x35,0 x1c,0 x2f);CoInitialize();Ia*pIx=NULL;HRESULT hr=:CoCreateInstance(CLSID_COM1,NULL,CL
15、SCTX_INPROC_SERVER,IID_IX,(void*)&pIx);if(SUCCEEDED(hr)pIx-Fx();pIx-Release();extern C const GUID 其实是所谓的全局唯一标示符(Globally Unique Identifier)。我们规定用它来表示不同的接口。换而言之,如 果你发现有两个 GUID 完全相同,你完全有理由相信他们标示的是同一个 接口。(有专门的算法来产生该结构,确保它在时间和空间上都是唯一的。)接下来的 CoInitialize 函数初始化 COM 库。这一步是非常重要的,如果 没有初始化,以后进行的操作都将失败。下面我们来看看
16、 HRESULT 值。这是一个 32 位的返回值,其最高位表示函 数调用是否成功。第十六位包含的就是函数的返回值,其余的 15 位包含的 是此类型以及返回值起源的更详细信息。为了确定函数调用是否成功,需 要使用 SUCCEED 和 FAILED 宏。CoCreateInstance 函数是由 COM 函数库提供的,它的作用就是按照查询的 组件和接口到系统中去寻找其所在的文件(一般总是 EXE 或者 DLL 文件)然 后创建该组件并查询其接口。一般来说,这个函数的具体实现是与系统相 关的,以后将会提到,在 windows 系统中,将查询注册表已确定某个特定 的组件在哪个文件中。上例查询的是 CL
17、SID_COM1 这个组件,由于一个组件可能包含有多个接口,所以我们使用 IID_IX 来制定所需要的接口,CLSCTX_INPROC_SERVER 是一个 常数,其指定组件所在的是一个 DLL(由于 DLL 运行在客户的内存空间,所 以可以称为是进程内组件)。最后一个参数传入的是接口指针,它将返回 查询到的接口指针。可以想见,一个组件指针可能同时被几个客户所使用,所以需要一种手 段来让组件实例知道自己正在被几个客户所使用,这样他才能再合适的时 候销毁自己以让出内存空间。如果销毁的实际不当,比如还有个指针正在 使用中,那么以后对该指针的调用就将失败并且用户程序将崩溃。COM 采用 相当简单的一
18、种手段来进行所谓的引用计数:维护一个组件或接口的全局 变量,但该变量的值为零时,销毁自己的时间就到了。CoCreateInstance 实际上产生了该组件的实例,并在内部已经调用 IUnknown 的 AddRef()函数 来将引用计数置 1 了。正因为如此,例子最后调用的 Release()函数就是做 了清理工作:这个接口指针已经完成了它的工作,所以调用 Release()告 诉它:把你的引用计数减一。如果不这样做,组件将永远保留在内存中,直到应用程序结束的时候才从栈中被清除。对 AddRef 和 Release 函数的调用是为了更好的控制组件的生命期,当然如 果处理得当,可以适当的减少 A
19、ddRef/Release 对以提高性能。一种特殊的 情况就是当一个组件的生命期完全被包含在另一个组件内时,我们对被包含 的那个组件可以不予计数。我不准备详细讨论优化问题,因为对于一般应 用来说,保证程序的强壮和稳定才是最重要的。这里还得介绍一下 ProgID。ProgID 其实程序员给某个 CLSID 指定的易记 名。某些语言如 visual basic 使用 ProgID 而非 CLSID 来表示组件。这里请注 意,程序员对 ProgID 的命名只不过是遵循一个约定俗成的规定,并没有对 具体的实现有任何的强制标准,所以其名字发生冲突也是有可能的。一般 来说,ProgID 具有如下格式:.以
20、我的注册表为例:INSHandler.INSHandler.1 ImgUtil.CoSniffStream.1 StaticMetafile Netscape.Help.1 不过由于 ProgID 没有专门的命名规则,所以出现不同于上述格式的名字 也是完全有可能的。有时候客户并不关心它所连接的组件版本,换而言之,客户只需要知道该组件存在就心满意足了。所以,组件经常会有一个与版 本无关的 ProgID,此 ProgID 被映射成是所安装的最新版本的组件。完成从 ProgID 到 CLSID 的转换非常简单,只需要利用 COM 库中提供的两个函 数 CLSIDFromProgID 和 ProgID
21、FromCLSID 就可以了。CLSID clsid;CLSIDFromProgID(LNetscape.Help.1,&clsid);上面的 L是一个扩展宏,用来转换普通的 ANSI 字符串成为 Unicode 串。下面需要讨论的问题是:假设现在我已经写好了一个组件,怎么才能在 注册表中登记它的接口呢?非常简单,我们只需要在组件中实现下面两个 函数就可以了。_declspec(dllexport)DllRegisterServer();_declspec(dllexport)DllUnregisterServer();具体而言,DllRegisterServer 的实现实际上是通过直接调用注
22、册表函数来 实现的。为了注册或者取消某个组件的注册,需要用的函数一般有:RegOpenKeyEx RegCreateKeyEx RegSetValueEx RegEnumKeyEx RegDeleteKey RegCloseKey 使用这些函数是需要#include 或者,并在 additional librarys 里加上 advapi32.lib。现在的一个问题是:客户怎样选择自己所需要的组件呢?开发人员需 要的是一种无需创建组件实例就能知道它是否能提供所需接口的方法。轮询系统中的所有组件和接口不失为一种解决的方法,但这样做的系统 开销相当大。为此引进了称为组件类别的方案。一个组件类别实际
23、上就是一个接口集合。我们分配给该集合一个 GUID 以唯一的标示它,它被称作 CATID。对于任何一个组件,如果它实现了某 个组件类别的所有接口,那么它就可以把自己注册成是该组件类别的一 个成员。这样一来,客户只需要选择合适的组件类别并查询其下所有列 出的组件就可以了。对组件而言,并不限制它只能属于一个组件类别。反过来,属于某个组件类别的组件并不限于只实现改组件类别中的接口。如果乐意,你可以写一个组件支持实现所有组件级别并且还有额外的接 口。组件类别是怎样被实现的?使用 Component Category Manager(由 windows 提供),它是一个实现了 ICatRegister
24、和 ICatInformation 接口的 组件。ICatRegister 可以完成新组件类别的登记或取消,也可以将某个组 件登记入某个组件类别,或取消之。ICatInformation 则可以用来获取系 统中某个组件类别的数据。组件中分配了一块内存,然后建起通过一个参数(可能是一个返回的指 针)传递给了客户,这是一种非常常见的做法。问题是:谁来释放这块 内存?这主要是由于组建和客户可能是有不同的程序员实现的,他们之 间没有办法建立一种分配和释放内存的标准办法。COM 解决中各问题的办 法是提供一个接口(IMalloc),它可以有 CoGetMalloc 返回。为了分配 内存,只需要调用 IM
25、alloc:Alloc,而调用改函数所分配的内存可以有 IMalloc:Free 负责释放。为了更加简单的实现,COM 库提供了两个更加 简单的函数:void CoTaskMemAlloc(ULONG cb /*size in bytes of block to be allocated*/);和 void CoTaskMemFree(void*pv);如果你认真看了我的文章,到现在为止你大体上已经有了一个概念:COM 究竟是一种什么概念,它在哪些程度上需要程序员来实现,哪些则是 由操作系统所提供的 COM 库完成的。不十分严格的说,COM 的目的是把各 种各样的函数分类,然后封装成一个个物件
26、,这些物件在 windows 系统中 以 DLL 或者 EXE 的形式具体存在,并且通过注册表,window 随时随地可以 知道某个特定组件的代码是在那个对应得 DLL 或者 EXE 里。这里提一下,怎么告诉 windows 你需要哪个组件呢?我们使用 GUID,其复杂的算法保证 了世界上没有两个个接口的 ID 标示号码是完全一样的!从而可以唯一的 确定组件,包括内含的接口,在客户需要该组件的时候 windows 也就可以 正确的装载它了。同样也是因为这个唯一性,客户在任何时候都可以直 截了当的,明确无误的询问 windows,我要的就是这个组件里的这个接 口!告诉我你有吗?这时候,通过一个
27、CoCreateInstance 函数,windows 将返回接口指针,或者干脆的告诉你,没有找到!那么,windows 内部在执行这个函数的时候具体做了些什么呢?首先它 查询了注册表,找寻你所要的组件(组件也就是接口集,而所谓接口也 就是一组函数所组成的集合的代名词,这么说你明白了吗?)如果没有 找到该组件,查询自然失败,函数返回,如果找到了,那么进一步的,内核将向 windows 返回该组件的 IUnknown*指针,windows 随后利用 IUnknown:QueryInterface 函数查询你所指定的那个接口是不是被该组件 所实现(或者说支持)说到这里你一定可以发现,凡是接口,一般来说总 是要由你的代码去实现,IUnknown 这个所有 COM 组件都必须实现的接口,其目的之一就是让 Windows 知道如何查询你的组件。直到组件里实现了哪 些接口的只有你自己-写这个组件的人,所以你有责任妥当的好好些 QueryInterface 函数以便返回正确的指针,windows 随后将该指针转给 CoCreateInstance 的调用处,整件事情也就结束了.