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