更新时间:2023-12-07 11:38
XPCOM是一个跨平台组件模型,它的全称为Cross Platform Component Object Module。XPCOM实现了一个框架(framework),这个框架中,它允许开发者打破单一整体的软件项目,而分解为多个更小的模块化碎片(pieces),这些碎片也即组件(components)。
XPCOM: Cross Platform Component Object Module 跨平台组件对象模块。基于Mozilla的浏览器现在可以通过使用跨平台组件对象模块(Cross Platform Component Object Module,XPCOM)组件添加它们自己的功能。XPCOM是一个框架,它允许对项目进行模块化设计,将整个开发划分为多个组件。组件然后在运行时期重新装配,创建出组件的克隆共同创建一个应用程序。
XPCOM简介
本文简单介绍了 XPCOM 和组件开发的基本概念和技术. 本文的大部分内容从一个很高的角度介绍了这些概念, 这些概念将在创建一个称为 WebLock 的 Mozilla 组件过程中逐渐透彻的讲述.
XPCOM 解决方案
Cross Platform Component Object Module (XPCOM) 是一个允许开发人员把一个大的工程划分成小的模块的框架. 这些小模块称为组件, 它们在运行时刻组装在一起.
XPCOM 的目标是使软件的不同部分分别开发, 相互独立. 为了是应用的不同组件之间能够互操作, XPCOM 把组件的实现与接口(后面讨论 接口) 分开. 同时 XPCOM 还提供了加载和操纵这些组件的库和工具以及服务, 以帮助开发人员编写跨平台的代码和组件版本管理; 因此组件可以在不破坏应用或者重新生成应用的同时被替换被更新. 通过使用 XPCOM, 开发人员创建的组件可以在不同的应用中被重用, 或者替换当前应用中的组件以改变应用的功能.
XPCOM 不只提供组件软件开发的支持, 它同时提供一个开发平台的大多数功能的支持:
组件管理
文件抽象
对象消息传递
我们将在后面的章节中详细讨论上面的条目, 但是现在, 仅仅把 XPCOM 想象成一个 组件开发平台, 它提供了上面的这些特性.
Gecko
XPCOM 尽管在许多方面类似于 Microsoft COM, 但 XPCOM 被设计成主要应用于应用层. XPCOM 的最重要的应用是 Gecko, 一个开源的, 遵循标准的, 嵌入式 web 浏览器和 工具包.
XPCOM 是访问 Gecko 库和嵌入或扩展 Gecko 的方法. 本书着重于后者 - 扩展 Gecko - 但是本书中描述的基本思想对嵌入 Gecko 的开发人员也很重要.
Gecko 应用在许多 internet 程序中, 主要是浏览器. 包括 Gateway/AOL Instant AOL device 和 Nokia Media Terminal. Gecko 最近还被应用于的 Compuserve 客户端, AOL for Mac OS X, Netscape 7, 当然还包括 Mozilla 客户端. Gecko 是目前而言主流的开源浏览器.
组件
XPCOM 允许把一个大的工程分解为一些小部分. 这些小部分称为组件, 通常是一些小的可重用的二进制库(在 Windows 下表现为一个 DLL , Unix 下为一个 DSO ), 这些二进制库可以包含一个或多个组件. 当多个相关组件被封装到一个二进制库, 这个库也称为模块.
把软件划分成不同的组件可以使开发和维护工作变得更容易. 除了这个好处, 模块化组件化的编程还有下面的优势:
优点 描述
重用 模块化的代码可以在其他应用和环境中被重用.
更新 在不需要重新编译整个应用的情况下更新组件.
维护 甚至在不更新组件的情况下, 按照模块化的方式设计应用也能够使你对感兴趣的部分的维护变得更容易.
但是把事物划分开来并不一定就好, 有许多事物作为一个整体组织起来才是最好的方式, 或者根本就不能划分开来. 比如说, 一个小孩就可能不吃没有果酱的三明志, 对于他来说, 果酱和三明志是缺一不可的整体. 同样的情形也会存在于某些软件中, 某些需要紧耦合的仅仅在内部使用的部分就并不需要费力去把它拆开成为小的模块.
Gecko 中的 HTTP 组件并不会暴露它内部的类, 它是作为一个整体来使用. 组件内部的事物不应该暴露给 XPCOM. 在 Mozilla 早期的开发中, 有些组件的创建并不适当, 但是随着开发的深入, 这些部分会被逐渐移出 XPCOM.
接口
把软件划分成组件通常一个好的解决方法, 但是我们该怎么做呢? 基本的思路是按照功能进行划分, 理解这些功能块之间的如何进行通信. 这些组件之间的通信通道就构成了各个组件的边界, 这些边界形式的描述为接口.
接口允许开发人员把功能的具体实现封装在组件内部, 而客户程序不需要了解这个功能是如何实现的, 它只需要使用它就行了.
接口与按照契约(Contract)编程
一个接口在组件与客户程序之间达成契约. 并没有任何强制措施保证认同这个契约, 但是忽略它会是致命的. 在基于组件的编程中, 组件保证它提供的接口是不变的 - 不同版本的组件都要提供同样的访问方法 - 这就与使用它的客户程序达成了一种契约. 从这种角度来说, 基于组件的编程通常也称为按照契约编程.
接口与封装
组件边界之间的抽象对软件的可维护性与可重用性是致关重要的. 举个例子来说, 考虑下面一个没有很好封装的类. 在下面的例子中, 使用可自由访问的 public 的初始化方法可能会出问题.
SomeClass类初始化
class SomeClass{ public: // Constructor SomeClass(); // Virtual Destructor virtual ~SomeClass(); // init method void Init(); void DoSomethingUseful();};系统要工作正常, 客户程序员必须注意到组件程序员建立的潜规则. 这就是这个未很好封装的类的契约: 这是一套关于何时该调用一个方法, 调用这个方法之前应该做什么的规则. 这个规则可能指出 DoSomethingUseful 方法只能在调用 Init() 之后被使用. DoSomethingUseful 方法可能会做某些检查工作以保证条件满足 - Init 已经被调用.
除了在代码中给出注释告诉客户程序员关于 Init() 规则之外, 程序员可以使他的契约更明晰. 首先对象的构造函数可以封装起来, 然后向客户程序员提供一个声明 DoSomethingUseful 方法的虚基类. 通过这种方式, 构造函数和初始化函数被隐藏起来. 在这种半封装条件下, 这个类只向客户程序暴露一些良好定义的可调用方法(或者称为接口). 一旦按照这种方式封装一个类, 客户程序只能看到的是下面的接口:
SomeInterface封装
class SomeInterface{ public: virtual void DoSomethingUseful() = 0;};实现类就可以从这个虚基类派生, 实现接口的功能. 客户程序使用类厂(factory pattern)模式来创建对象(参看 类厂), 而封装类的内部实现. XPCOM 以这种方式把客户程序屏蔽在组件的内部工作之外, 而客户程序也只依赖于提供所需要功能的接口.
nsISupports 基接口
组件与基于接口的编程的两个最基本的问题是: 一个是组件生存期, 也称为对象所属关系; 另一个是接口查询, 它是在运行时刻确定接口能够提供哪些接口. 这一节介绍基接口 nsISupports - 它是 XPCOM 中所有接口的父接口, 它提供了上面两个问题的解决方案.
对象所属关系
在 XPCOM 中, 由于组件可以实现任意多的不同接口, 接口必须是引用计数的. 组件在内部跟踪客户程序对它的接口引用了多少次, 当接口计数为零的时候组件就会卸载它自己.
当一个组件创建后, 组件内部有一个整数在跟踪这个引用计数值. 客户程序实例化组件就会自动对这个引用计数加一; 在组件的整个生存期内, 引用计数或加或减, 总是大于零的. 如果在某个时刻, 所有的客户程序对该组件都失去了兴趣, 引用计数减到零, 组件就会卸载自己.
客户程序使用相关接口是一个非常直接的过程. XPCOM 有一些工具让我们更方便的使用接口, 我们会在后面讲述. 如果客户程序在使用接口的时候忘记对接口的引用计数进行相关操作, 就会对组件的维护工作带来某些问题. 此时, 由于组件的引用计数始终不为零, 它就永远不会释放, 从而导致内存泄漏. 引用计数系统就象 XPCOM 的许多其他事物一样, 是客户与组件之间的契约. 如果遵守这些契约, 就会工作得很正常, 反之不然. 由创建接口指针的函数负责对初始化的接口引用加1, 这个引用也称为所属引用.
XPCOM中的指针
XPCOM 中的指针术语指的是接口指针. 它与常规指针相比有细微的差别, 毕竟它们都指向的是某个内存区域. 但是 XPCOM 指针指向的都是从 nsISupports 基接口派生而来的接口实现, 这个基接口包括三个基本的方法: AddRef, Release, 和 QueryInterface.
nsISupports 接口提供了对接口查询与引用计数基本的支持. 这个接口的成员方法包括: QueryInterface, AddRef, 和 Release. 这些方法提供了从一个对象获取正确接口的基本方法, 加引用计数, 释放不再使用的对象. nsISupports 接口的声明如下:
nsISupports 接口
class Sample: public nsISupports{ private: nsrefcnt mRefCnt; public: Sample(); virtual ~Sample(); NS_IMETHOD QueryInterface(const nsIID &aIID, void **aResult); NS_IMETHOD_(nsrefcnt) AddRef(void); NS_IMETHOD_(nsrefcnt) Release(void);};接口中使用的各种数据类型见 XPCOM 类型一节. nsISupports 接口的实现代码如下:
nsISupports 接口实现
Sample::Sample(){ // initialize the reference count to 0 mRefCnt = 0;}Sample::~Sample(){}// typical, generic implementation of QINS_IMETHODIMP Sample::QueryInterface(const nsIID &aIID, void **aResult){ if (aResult == NULL) { return NS_ERROR_NULL_POINTER; } *aResult = NULL; if (aIID.Equals(kISupportsIID)) { *aResult = (void *) this; } if (*aResult == NULL) { return NS_ERROR_NO_INTERFACE; } // add a reference AddRef(); return NS_OK;}NS_IMETHODIMP_(nsrefcnt) Sample::AddRef() { return ++mRefCnt;}NS_IMETHODIMP_(nsrefcnt) Sample::Release(){ if (--mRefCnt == 0) { delete this; return 0; } // optional: return the reference count return mRefCnt;}对象接口的发现
继承是面向对象编程中另一个非常重要的话题. 继承是通过一个类派生另一个类的方法. 当一个类继承另一个类, 继承类可以重载基类的缺省动作, 而不需要拷贝基类的代码, 从而创建更加专有的类. 如下所示:
简单类继承
class Shape{ private: int m_x; int m_y; public: virtual void Draw() = 0; Shape(); virtual ~Shape();}; class Circle : public Shape{ private: int m_radius; public: virtual Draw(); Circle(int x, int y, int radius); virtual ~Circle();};Circle 从 Shape 类派生. Circle 本身也是一个 Shape, 但是 Shape 并不一定是 Circle. 在这种情况下, Shape 是基类, Circle 是 Shape 的子类.
在 XPCOM 中, 所有的类都派生自 nsISupports 接口, 这样所有的对象都提供 nsISupports接口, 但是这些对象是更专有的类, 在运行时刻必须能找到它. 比如说在 简单类继承例子中, 如果对象是一个 Circle, 你就可以调用 Shape 类的方法. 就是为什么在 XPCOM 中, 所有的对象都派生自 nsISupports 接口: 它允许客户程序根据需要发现和访问不同的接口.
在 C++ 中, 我们可以使用 dynamic_cast<> 来把一个 Shape 对象的指针强制转化成一个 Circle 指针, 如果不能转化就抛出异常. 但是在 XPCOM 中, 由于性能开销和平台兼容性问题, 采用RTTI 的方法是不行的.
XPCOM 中的异常
XPCOM 并不直接支持 C++ 的异常处理. 在 XPCOM 中, 所有的异常必须在组件内部处理, 而不能跨越接口的边界. 然后接口方法返回一个 nsresult 错误值(这些错误码
XPCOM 没有采用 C++ RTTI 机制来实现对象指针的动态转化, 它使用 QueryInterface 方法来把一个对象指针 cast 成正确的接口指针.
当客户程序想查询一个对象是否支持某个接口, 它把接口的 IID 值传递给这个对象的 QueryInterface 方法. 如果对象支持这个接口, 它就会对自己的引用计数加1, 然后返回指向那个专有接口的指针. 反之, 如果不支持这个接口, 它会返回一个错误码.
class nsISupports { public: long QueryInterface(const nsIID & uuid, void **result) = 0; long AddRef(void) = 0; long Release(void) = 0;};QueryInterface 的第一个参数是一个 nsIID 类型的引用, 它封装了 IID. nsIID 类有三种方法: Equals, Parse, 和 ToString. Equals 在接口查询中是最重要的, 它用来比较两个 nsIID 对象是否相同.
在客户以 IID 调用 nsISupports 接口的 QueryInterface 方法时, 我们必须保证返回一个有效的 result 参数(在Using XPCOM Utilities to Make Things Easier 一章中, 我们将看到怎样更方便的实现一个 nsIID 类). QueryInterface 方法应该支持该组件所有接口的查询.
在 QueryInterface 方法的实现中, IID 参数与组件支持 nsIID 类进行比较. 如果匹配, 对象的 this 指针转化为 void 指针, 引用计数加1, 把 void 指针返回给客户程序.
在上面的例子中, 仅仅使用 C 方式的类型转化就足够了. 但是在把 void 指针 cast 成接口指针的时候, 还有更多的问题, 因为返回的接口指针必须与 vtable 相对应. 当出现菱形多重继承的时候, 可能这种接口转化就会有问题.
XPCOM 的ID
除了上一节中的接口 IID, XPCOM 还使用两种 ID 来区分类与组件.
XPCOM ID类
nsIID 类实际上是一种 nsID 类. 其他的 nsID, CID 和 IID, 分别指的是一个实体类和一个专有的接口.
nsID 类提供 Equals 类似的方法, 来比较 ID. 请参考 Identifiers in XPCOM 一节, 其中对 nsID 类有更多的讨论.
CID
CID 是一个唯一的 128-bit 值, 类似于 IID, 它用于全局标识唯一的类或者组件. nsISupports 的 CID 就象:
00000000-0000-0000-c000 000000000046
由于 CID 比较长, 通常我们都使用#define来定义它:
静态变量:
static NS_DEFINE_CID(kWebShellCID, NS_WEB_SHELL_CID);CID 有时也称为类 ID. 如果一个类实现了多个接口, 当CID 在该类发布后, 就与该类的 IID 一起被冻结了.
契约 ID
契约 ID 是一个用于访问组件的可读(to humen)的字符串. 一个 CID 或者一个契约 ID 可以用于从一个组件管理器获取组件. 下面是一个用于 LDAP 操作组件的契约 ID:
与 CID 类似, 契约 ID 指的是组件实现而不是接口. 但是契约 ID 并不像CID那样,被限定为某个专有实现, 它更通用. 一个契约 ID 只是指明需要实现的一组接口,可以通过任意数量的CID满足这个需要. 契约 ID 与 CID 的这种不同, 使得组件重写成为可能.
把代码划分成组件, 客户代码通常使用 new 操作符来构造实例对象:
SomeClass* component = new SomeClass();这种模式或多或少地需要客户对组件有一定的了解,至少要知道组件的大小. 类厂设计模式此时可以用来封装对象的构造过程. 类厂模式的目的是在不暴露对象的实现和初始化的前提下创建对象. 在 SomeClass 例子中, 可以按照类厂模式把 SomeClass 对象的构造和初始化封装在 New_SomeInterface 函数中:
封装构造函数
int New_SomeInterface(SomeInterface** ret){ // create the object SomeClass* out = new SomeClass(); if (!out) return -1; // init the object if (out->Init() == FALSE) { delete out; return -1; } // cast to the interface *ret = static_cast
封装构造函数 的例子是一个简单的无状态的类厂版本, 实际的编程要复杂一些, 一般的类厂都需要保存状态. 类厂至少应该保存那些对象已经被创建了的信息. 如果类厂管理的实例被存放在一个动态联接库中, 还需要知道什么时候可以卸载这个动态联接库. 当类厂保存了这样的信息, 就可以向类厂查询一个对象是否已经被创建.
另一个需要保存的信息是关于单件(singleton). 如果一个类厂已经创建了一个单件类型的类, 后续的创建该单件的函数调用将返回已经创建的对象. 尽管有更好的工具和方式来管理单件(在讨论 nsIServiceManager 会看到), 开发人员可能仍然需要通过这种方式来保证只有一个单件对象被创建.
厂模式可以完全利用函数来做, 状态可以保存在全局变量中; 但是使用类的方式来实现厂模式还有更多的好处. 其一是: 我们可以管理从 nsISupports 接口派生而来的类厂
XPIDL 与类型库
定义接口的简单而强劲的方法是使用接口定义语言(IDL) - 这实际上是在一个跨平台而语言无关开发环境下定义接口的需求. XPCOM 使用的是源自于 CORBA OMG 接口定义语言(IDL)的变体, 称为 XPIDL, 来定义接口, XPIDL 可以定义接口的方法, 属性, 常量, 以及接口继承.
采用 XPIDL 定义接口还存在一些缺陷. 它不支持多继承, 同时 XPIDL 定义的方法名不能相同,你不能有两个相同名字但是所接受的参数不同的函数. - 不能像 C++ 语言的成员函数一样通过参数不同重载, 毕竟接口同时要支持类似于 C 这样的语言.
void FooWithInt(in int x);void FooWithString(in string x);void FooWithURI(in nsIURI x);然而尽管有这些问题, XPIDL 的功能仍然是非常强大的. XPIDL 能自动生成以 .xpt 为后缀的类型库, 或者说 typelibs. 类型库是接口的二进制表示, 它向非 C++ 语言提供接口的访问与控制. 非 C++ 语言通过类型库来了解接口支持哪些方法, 如何调用这些方法, 这称为 XPConnect. XPConnect 是 XPCOM 向类似于 JavaScript 这样的语言提供组件访问的 Wrapper. 参看Connecting to Components from the Interface获取更多关于 XPConnect 的信息.
从其他类型的语言访问接口, 常常说成是接口被反射(reflected)到这种语言. 每一个被反射的接口必须提供相应的类型库. 当前可以使用 C, C++, 和 JavaScript 来编写组件.
使用其他语言编写组件
在使用其他语言创建组件的时候, 不需要利用 XPCOM 提供给 C++ 程序员的工具(诸如一些宏, 智能指针等等, 我们可以方便的利用到这种语言本身来创建组件. 比如说, 基于 Python 的 XPCOM 组件可以从 JavaScript 来调用.
参看 Resources 获取更多使用其他语言来创建组件的信息.
所有的 XPCOM 接口都用 XPIDL 语法来定义. xpidl 编译器会从这个 IDL 文件产生类型库和 C++ 头文件. 在Defining the WebLock Interface in XPIDL 一节详细描述了 XPIDL 语法.
XPCOM 服务
当客户需要某个组件提供的功能的时候, 通常都是实例化一个新的组件对象. 比如说, 客户程序需要某些处理文件, 这里每个组件就代表一个文件, 客户可能会同时处理多个这样的组件.
但是在某些情况下对象表示的是一种服务, 这种服务只能有一个拷贝(尽管会有多个服务同时运行). 每次客户程序访问服务提供的功能时, 客户程序都是与同一个服务实例在打交道. 比如说, 一个用户查询公司的电话号码数据库, 数据库作为一个对象对用户来说都是同一的. 如若不然的话, 就需要维护两个数据库拷贝, 这种开销将非常大, 而且还存在数据内容不一致的问题. 单件设计模式就是提供系统中需要的这种单点访问功能.
XPCOM 不仅提供了对组件的支持和管理服务, 它还包含了许多编写跨平台组件所需要的其他服务. 其中包括: 跨平台文件抽象, 向 XPCOM 开发人员提供同一而强大的文件访问功能. 目录服务, 提供应用和特定系统定位信息. 内存管理, 保证所有对象使用同样的内存分配器. 事件通知机制, 允许传递简单消息. 本教程将在后面展现如何使用这些服务, XPCOM API Reference 一节有完整的接口索引列表.
XPCOM 类型
XPCOM 声明了许多数据类型和简单宏, 这些东西将在我们后面的例子中看到. 大多数的宏都是简单的重定义, 下一节我们会描述一些最常用的数据类型.
方法类型
下面的类型用在 XPCOM 方法调用的参数和返回值定义中.
NS_IMETHOD 声明方法返回值. XPCOM 的方法缺省的返回值声明.
NS_IMETHODIMP 方法实现返回值. XPCOM 方法函数返回的时候缺省采用这种类型的返回值.
NS_IMETHODIMP_(type) 特定类型的方法实现返回值. 诸如 AddRef 和 Release 的方法不返回缺省类型, 这种返回值的不一致虽然有点不舒服, 但是必需的.
NS_IMPORT 共享库内部使用的符号局部声明
NS_EXPORT 共享库导出的符号声明.
下面的宏提供对引用计数的基本操作.
NS_ADDREF 调用 nsISupports 对象的 AddRef 方法.
NS_IF_ADDREF 与上一个方法类似, 不同之处在于这个宏在AddRef之前会检查对象指针是否为空(虚函数指针).
NS_RELEASE 调用 nsISupports 对象的 Release 方法.
NS_IF_RELEASE 与上一个方法类似, 不同之处在于这个宏在调用Release之前会检查空指针.
状态码
下面的宏测试状态码.
NS_FAILED 如果传递的状态码为失败, 则返回真.
NS_SUCCEEDED 如果传递的状态码为成功, 则返回真.
变量映射
nsrefcnt 缺省的引用计数类型, 是一个 32-bit 整数.
nsresult 缺省数据类型, 是一个 32-bit 整数.
nsnull 缺省 null 类型.
通用 XPCOM 错误码
NS_ERROR_NOT_INITIALIZED 如果实例未初试化, 返回该值.
NS_ERROR_ALREADY_INITIALIZED 如果实例已初试化, 返回该值.
NS_ERROR_NOT_IMPLEMENTED 如果方法未实现, 返回该值.
NS_ERROR_NO_INTERFACE 如果组件不支持某种类型接口, 返回该值.
NS_ERROR_NULL_POINTER 如果指针指向 nsnull, 返回该值 .
NS_ERROR_FAILURE 如果某个方法失效, 返回该值, 这时一个通用的错误值.
NS_ERROR_UNEXPECTED 如果一个未预料的错误发生, 返回该值.
NS_ERROR_OUT_OF_MEMORY 如果无法进行内存分配, 返回该值.
NS_ERROR_FACTORY_NOT_REGISTERED 如果一个请求的类型未注册, 返回该值.