XPCOM解决方案
XPCOM允许开发者把软件项目分解成模块,这就是所谓的组建,在运行时被组装在一起。XPCOM的目标就是使得各个模块可以独立开发。为了组件在程序中的互操作性,XPCOM把组件的实现和接口分离。但是XPCOM也提供一些工具和库用于加载和操作组件和服务,使得开发者能写出支持版本和跨平台的代码,这样组件就可以被替换或升级而不是重建程序。使用XPCOM,开发者开发的组件可以在不同的程序使用,或者替换程序的功能
XPCOM不仅仅支持组件开发,还提供了平台开发的大部分功能,比如:
1.组件管理
2.文件抽象
3.对象消息传递
4.内存管理
以下章节将详细介绍,现在,我们只需要把XPCOM看成是一个组件开发平台。
Gecko
即使在某些方面和微软的COM结构相似,XPCOM设计用于应用程序级别的。XPCOM最主要用在Gecko。XPCOM是访问Gecko库和嵌入或者扩展Gecko的方法。
Gecko用在很多网络应用程序,最多的是浏览器。
Components
XPCOM允许你创建一个大系统,把它分解成多个模块。这就是组件,很小的,可重用的二进制库,可以包含一个或多个组件。当一个二进制库包含两个或者多个相关的组件,这个库则称为模块。把软件分为多个组件方便开发和维护。
Interfaces
把软件分解为多个组件是好的想法,正确的做法如何呢。最基本的思想是识别模块的功能和理解模块之间的通信。模块之间的通道构成了模块之间的边界,边界正式化之后就是所谓的接口。
接口在编程中并不是一个新想法。在我们的第一个“hello world”程序中就已经用到接口。接口允许开发者封装实现和软件内部工作机制。客户不需要了解事情是怎么做的。
Interfaces and Encapsulation
组件之间的边界,抽象对软件维护和重用至关重要。假设,一个类封装不好,使用了公开的初始化方法,
class SomeClass
{
public:
// Constructor
SomeClass();
// Virtual Destructor
virtual ~SomeClass();
// init method
void Init();
void DoSomethingUseful();
};
该系统要正常工作,程序员必须关注组件是怎么创建的。该未封装的类的契约 是:一组定义方法什么时候可以调用和用于做什么的规则。一条规则指定 在调用 Init()之后才能调用DoSomethingUseful。DoSomethingUseful做一些检查,确保
Init()已经先被调用。
除了编写良好的注释代码告知开发者init的规则之外,开发者还需要使得契约更简洁。首先,对象的创建可以被封装,DoSomethingUseful定义为虚函数。这样创建和初始化对用户就完全是不可见的。在这种半封装的情况下,类唯一暴露的部分只是一组可调用的函数。一旦类被封装,用户可见的接口如下:
class SomeInterface
{
public:
virtual void DoSomethingUseful() = 0;
};
实现可以继承该类并实现虚函数。代码的使用者可以用工厂模式创建一个对象并进一步封装实现。在XPCOM中,通过这种方法对客户隐藏组件内部实现,依赖接口访问提供的功能。
The nsISupports Base Interface
在组件和基于接口的编程中根本的两个问题是组件生命周期,也叫对象所有权和接口查询,或能够识别哪个接口组件在使用。本节介绍基本的接口,XPCOM中所有接口的母接口——nsISupports。它为以上的两个问题提供了解决方法。
Object Ownership
在XPCOM中,由于组件可能实现多个接口,接口必须引用计数。组件必须记录有多少客户端与之关联,当引用计数为0时将自己删除。当创建组件时,内部的整型数进行计数。客户端实例化组件时引用计数自动自增,在组件的生命周期,引用可以增加或减少,一直大于0.某种情况下,所有的客户端不再需要组件,引用计数变位0,组件将自身删除。
当客户端负责人的使用接口,这个是很简单的过程。XPCOM有工具使之更为简单。它可能引发一些管理问题,比如,一个客户端使用了一个接口,但是忘记把引用计数减少。这样的话接口就不会被释放而导致内存泄漏。引用计数系统就像客户端和实现之间的契约。如果遵守则能很好的工作,否则就会出错。创建接口指针的函数要负责初始化引用计数。
nsISupports支持了接口查询和引用计数的基本功能。QueryInterface , AddRef , 和Release 提供了基本的方法来从对象中获取接口,增加引用计数和释放对象。
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);
};
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;
}
Object Interface Discovery
继承是面向对象编程的一个重要话题,继承就是一个类从另外一个类派生。当一个类继承了另外一个类,继承类可以重写父类的实现,从而创建更具体的类。
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();
};
在XPCOM中,所有的类都继承了nsISupports,所以所有的对象都是nsISupports对象,但也是更具体的类,可以在运行时查找到。如以上代码,你可以询问Shape 是否是 Circle ,并当成Circle 来使用。在XPCOM中,这就是 nsISupports 接口中 QueryInterface 的特性。它允许用户根据需要查找并访问不同的接口。 在C++中, 你可以用高级的特性 dynamic_cast<>,当 Shape 不能转化为 Circle会抛出异常。由于许多平台上的性能和兼容性使得启用异常和RIIT不可行,所有XPCOM以不一样的方式实现。不用C++ RTTI,XPCOM用QueryInterface方法正确的把对象转为所支持的接口。每个接口都分配 了一个从uuidgen工具生成的标识,全局唯一标识符(UUID)是唯一的,128位的数字。在接口的上下文环境中称为IID。
当客户端要查找一个对象是否支持一个接口时,只需给对象的QueryInterface接口传入IID,如果对象支持接口,则把自身的引用计数加1,并把接口的指针返回。如果对象不支持接口,则返回erro。
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 。当你实现nsIID类,你必须确保当调用QueryInterface时类的方法返回有效的结果。QueryInterface必须支持组件支持的所有的接口。在QueryInterface 的实现中,IID检查nsIID 类。如果匹配,则组件的this指针转化为void,引用计数增加1,并把接口返回给调用者。如果不匹配,类返回错误,输出参数设置为null。
在以上的例子中,你用C类型转换很容易。但是转换很复杂,你void转化为请求的类型,因为你不行返回请求接口对应的vtable之指针。有模糊的继承结构时可能会有问题。
XPCOM Identifiers
除了以上提到的IID之外,XPCOM还用了另外两个标识符来区分类和组件。CID和Contract ID。
CID
CID是128数字唯一标识一个类或组件。nsISupports的CID大概如下00000000-0000-0000-c000-000000000046。
CID的长度使得在代码中很难处理,所有经常定义为宏
#define SAMPLE_CID \
{ 0x777f7150, 0x4a2b, 0x4301, \
{ 0xad, 0x10, 0x5e, 0xab, 0x25, 0xb3, 0x22, 0xaa}}
或者用NS_DEFINE_CID定义常量
static NS_DEFINE_CID(kWebShellCID, NS_WEB_SHELL_CID);
CID有时也被称为一个类的标识,如果CID标识的类实现多个接口,CID确保类发布的时候的实现整组接口。
Contract ID
Contract ID是由于访问组件的可读的字符串。CID 或者
contract ID 用于从组件管理器中获取组件,这就是LDAP操作组件的contract ID。
"@mozilla.org/network/ldap-operation;1"
contract ID的格式为,域,模块,组件名和版本名称有斜杠分割。
CID, contract ID和实现相关而不是接口,但是contract ID并不是和特定的组件绑定,和CID一样,是很普遍的。contract ID 指定一组要实现的接口,任意数量的接口可以填充请求。contract ID 和 CID区别就是什么让覆盖成为可能。
Factories
一旦代码分解成组件,客户端代码就用new 实例化一个对象。
SomeClass* component = new SomeClass();
这种模式要求客户端了解组件,至少是有多大。工厂模式可以封装对象的构造。工厂模式的目的就是创建对象,使得客户不需要了解对象的实现和构造过程。在SomeClass例子中,SomeClass的构造和初始化,实现了SomeInterface抽象类,包含在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接口。并使用了工厂模式,就像上面的例子一样,封装对象的构造和初始化。
以上的例子很简单而且是无状态的工厂,但是实际编程不会那么简单,而且工厂通常要保存状态,至少,工厂要记录已经创建了哪些对象。当一个工厂管理类的实例建在动态库中时,需要知道什么时候可以卸载库。当工厂保存状态时,你可以询问是否还要外部引用查看工厂是否创建任何对象。工厂可以保存的另外一个状态是对象是否是单例的。如果工厂创建的对象支持单例模式,对该对象的后续请求应该返回同一个对象。即使有工具和更好的方法处理单例模式,开发人员还是需要用这些信息确保对象只有一个实例存在。
工厂类的需求可以以严格的功能方式处理,状态保存在全局变量,但是使用工厂类也是有好处的。当使用一个类实现工厂的功能时,假如派生自nsISupports,它允许你管理工厂对象的声明周期。当你想把工厂类组合在一起和确定它们是否可以被卸载时就很重要了。使用nsISupports接口的另外一个好处就是你可以支持其他接口。比如我们即将讨论nsIClassInfo,一些工厂支持查询底层实现的信息,比如对象用什么语言写,对象支持哪些接口等。这种面向未来是一个关键优势。它的出现源于nsISupports。
XPIDL and Type Libraries
一个简单而又强大的方式定义一个接口,要在一个跨平台,语言中立的环境定义一个接口,需要用接口定义预约IDL。XPCOM使用自己的变体的CORBA OMG接口定义预语言——XPIDL,可以给接口指定方法,属性和常量,也可以指定接口继承。使用XPIDL定义自己的接口有一些缺点,不支持多继承。如果你定义一个接口,它不能继承多个接口。另外一个限制就是方法名必须唯一。不用有两个方法名相同,参数不同,变通的方案就是用多个方法名:
void FooWithInt(in int x);
void FooWithString(in string x);
void FooWithURI(in nsIURI x).
这些缺点与使用XPIDL获得的好处相比显得苍白。XPIDL允许你生成类型库,后缀为.xpt的文件,类型库是接口的二进制表示。它提供接口的编程控制和访问。这在非C++中使用接口至关重要。当组件从其他语言访问时,它们使用二进制类型库访问接口,获得支持的方法并调用。XPCOM的这方面叫做XPConnect。XPConnect是XPCOM的一个层次,该层次提供了从其他语言访问组件的方法,例如javascript。当用非C++语言访问组件,例如javascript,组件就好表现为那种语言。每个被反应的接口都有相应的类型库。目前,可以用C,C++,javascript,python编写XPCOM组件,目前正努力实现用ruby和perl创建XPCOM组件。
XPCOM的公开方法用 XPIDL语法定义,类型库和C++头文件有xpidl compiler工具从IDL文件生成。
XPCOM Services
当用户使用组件,需要组件提供的功能时,通常实例化一个组件对象。这时,客户端处理文件:每个分开的文件表示成不同的对象,一些文件对象可以在任何时候被使用。但是有一种对象称为服务,它总是只有一个副本。每当客户端需要访问服务提供的功能时,它们都是以同一个服务实例交互。例如,当客户需要从公司的数据库查询电话号码时,数据库对所有的用户都被表现为一个对象。不然,数据库需要在内存中保存两个副本,一方面,这也可能导致数据不一致性。单例模式就是提供了这种单点访问的功能,也是服务在程序中做的事情。在XPCOM中,除了组件支持和管理之外,还有一组服务帮助开发者编写跨平台组件。这些服务包括提供统一而强大的文件访问的跨平台文件抽象,
维护应用程序的位置和系统特定的位置目录服务,内存管理确保所有的用户都使用相同的内存分配器,允许传递简单信息的事件通知系统。
XPCOM Types
XPCOM定义很多类型
Method Types
以下一组类型确保XPCOM方法正确的调用和返回
NS_IMETHOD
方法声明返回类型,XPCOM方法声明应该用这种返回类型。
NS_IMETHODIMP
方法实现返回类型 ,XPCOM方法实现应该用这种返回类型。
NS_IMETHODIMP_(type)
特殊情况实现返回类型,
一些方法如AddRef和释放不返回默认的返回类型,
NS_IMPORT 强制方法从共享库内部解析
NS_EXPORT 强制方法导出到共享库
NS_ADDREF 在nsISupports
对象上调用AddRef
NS_IF_ADDREF 和NS_ADDREF 类似,只是在调用之前检查指针非空
NS_RELEASE 在nsISupports
对象上调用Release
NS_IF_RELEASE 和NS_RELEASE
类似,只是在调用之前检查指针非空
Status Codes
以下宏检测状态码
NS_FAILED 当传入的状态码是失败则返回true
NS_SUCCEEDED 当传入的状态码是成功则返回true
Variable mappings
nsrefcnt 默认的引用计数类型,映射为32位整数
nsresult 默认的错误类型,映射为32位整数
nsnull 默认的null类型
Common XPCOM Error Codes
NS_ERROR_NOT_INITIALIZED 当一个实例没初始化时返回该值
NS_ERROR_ALREADY_INITIALIZED 当一个实例已经初始化时返回该值
NS_ERROR_NOT_IMPLEMENTED 未实现的方法返回该值
NS_ERROR_NO_INTERFACE 不支持给定的接口时返回该值
NS_ERROR_NULL_POINTER 一个有效的指针为nsnull 时返回该值