XPCOM 解决方案
From https://developer.mozilla.org/en/Using_nsCOMPtr/Getting_Started_Guide
(SOURCE URL:http://www.mozilla.org/projects/xpcom/book/cxc/html/preface.html)
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), 这些二进制库可以包含一个或多个组件. 当多个相关组件被封装到一个二进制库, 这个库也称为模块.
把软件划分成不同的组件可以使开发和维护工作变得更容易. 除了这个好处, 模块化组件化的编程还有下面的优势:
优点
描述
重用
模块化的代码可以在其他应用和环境中被重用.
更新
在不需要重新编译整个应用的情况下更新组件.
性能
代码按照模块化组织, 模块就不必要立刻被加载, 而是以 "lazy" 方式加载, 或者根本不需要加载. 这样就可以提升应用的整体性能.
维护
甚至在不更新组件的情况下, 按照模块化的方式设计应用也能够使你对感兴趣的部分的维护变得更容易.
Mozilla 有几百万行代码, 没有那个人能够彻底搞明白整个代码的细节. 面对这样的一个系统, 最好的方式是把它划分成一些小的, 更容易管理的部分; 同时使用组件模型来编程, 把相关组件组织到各个模块中去. 比如说, Mozilla 的网络库就包含了 HTTP, FTP, 及其他协议的组件, 这些组件被塞到一个单独的库里. 这个库就是网络模块, 也就是通常所说的 "necko."
但是把事物划分开来并不一定就好, 有许多事物作为一个整体组织起来才是最好的方式, 或者根本就不能划分开来. 比如说, 一个小孩就可能不吃没有果酱的三明志, 对于他来说, 果酱和三明志是缺一不可的整体. 同样的情形也会存在于某些软件中, 某些需要紧耦合的仅仅在内部使用的部分就并不需要费力去把它拆开成为小的模块.
Gecko 中的 HTTP 组件并不会暴露它内部的类, 它是作为一个整体来使用. 组件内部的事物不应该暴露给 XPCOM. 在 Mozilla 早期的开发中, 有些组件的创建并不适当, 但是随着开发的深入, 这些部分会被逐渐移出 XPCOM.
[编辑]
接口
把软件划分成组件通常一个好的解决方法, 但是我们该怎么做呢? 基本的思路是按照功能进行划分, 理解这些功能块之间的如何进行通信. 这些组件之间的通信通道就构成了各个组件的边界, 这些边界形式的描述为接口.
接口并不是编程中的一个新的概念. 从我们的第一个 "HelloWorld" 程序开始我们就一直在不知不觉的使用它, 这里的接口就存在于我们的应用程序和打印程序之间. 应用程序使用 stdio 库提供的接口来把 "hello world" 字符串打印到屏幕上. XPCOM 与 "HelloWorld" 程序的不同之处在于 XPCOM 会在运行时刻找到这个屏幕打印的功能, 而 XPCOM 事前根本就不需要在编译的时刻了解 stdio库提供了什么东西.
接口允许开发人员把功能的具体实现封装在组件内部, 而客户程序不需要了解这个功能是如何实现的, 它只需要使用它就行了.
接口与按照契约(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 QI
NS_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 API Reference). 客户程序根据这些错误码进行"异常"处理.
XPCOM 没有采用 C++ RTTI 机制来实现对象指针的动态转化, 它使用 QueryInterface方法来把一个对象指针 cast 成正确的接口指针.
每个接口使用称为 "uuidgen" 的工具来生成专有ID. 这个 ID 是一个全局唯一的 128-bit 值. 在接口的语境中, 这个 ID 又称为IID. (组件语境中, 这个 ID 代表的是一个契约)
当客户程序想查询一个对象是否支持某个接口, 它把接口的 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来定义它:
#define SAMPLE_CID \
{ 0x777f7150, 0x4a2b, 0x4301, \
{ 0xad, 0x10, 0x5e, 0xab, 0x25, 0xb3, 0x22, 0xaa}}
我们将会看到许多 NS_DEFINE_CID的定义. 下面的宏用 CID 定义了一个静态变量:
static NS_DEFINE_CID(kWebShellCID, NS_WEB_SHELL_CID);
CID 有时也称为类 ID. 如果一个类实现了多个接口, 当CID 在该类发布后, 就与该类的 IID 一起被冻结了.
[编辑]
契约 ID
契约 ID 是一个用于访问组件的可读(to humen)的字符串. 一个 CID 或者一个契约 ID 可以用于从一个组件管理器获取组件. 下面是一个用于 LDAP 操作组件的契约 ID:
"@mozilla.org/network/ldap-operation;1"
契约 ID 的格式是: 用斜杠分开的组件的域, 模块, 组件名, 版本号.
与 CID 类似, 契约 ID 指的是组件实现而不是接口. 但是契约 ID 并不是某个专有实现, 它的用途更广泛. 一个契约 ID 只是指明需要实现的一组接口. 契约 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<SomeInterface*>(out);
return 0;
}
类厂本身是一个管理组件实例化的类. 在 XPCOM 中, 类厂要实现 nsIFactory接口, 它们就象上面的代码一样要使用类厂设计模式来封装对象的构造和初始化.
封装构造函数的例子是一个简单的无状态的类厂版本, 实际的编程要复杂一些, 一般的类厂都需要保存状态. 类厂至少应该保存那些对象已经被创建了的信息. 如果类厂管理的实例被存放在一个动态联接库中, 还需要知道什么时候可以卸载这个动态联接库. 当类厂保存了这样的信息, 就可以向类厂查询一个对象是否已经被创建.
另一个需要保存的信息是关于单件(singleton). 如果一个类厂已经创建了一个单件类型的类, 后续的创建该单件的函数调用将返回已经创建的对象. 尽管有更好的工具和方式来管理单件(在讨论 nsIServiceManager会看到), 开发人员可能仍然需要通过这种方式来保证只有一个单件对象被创建.
厂模式可以完全利用函数来做, 状态可以保存在全局变量中; 但是使用类的方式来实现厂模式还有更多的好处. 其一是: 我们可以管理从 nsISupports 接口派生而来的类厂本身的生存期. 当我们试图把多个类厂划分成一组, 然后确定是否能卸载这一组类厂的时候, 这一点非常重要. 另一个好处是: 类厂可以引入其他需要支持的接口. 在我们后面讨论 nsIClassInfo接口的时候, 我们会看到某些类厂使用这个接口支持信息查询, 诸如这个对象是用什么语言写的, 对象支持的接口等等. 这种派生自 nsISupports的 "future-proofing" 特性非常关键.
[编辑]
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
如果一个请求的类型未注册, 返回该值.

浙公网安备 33010602011771号