2008年6月13日星期五

浅谈Gecko关键部分之一XPCOM组件

为了便于统一认识下面将Gecko统称为Firefox内核引擎而不仅仅代表渲染引擎,为了能更加有效的加深对它的理解,能够从本质上了解它的构成,下面从个人认为最为关键的几个部分来分析了解它,争取通过对这几个关键部分的了解总结能让自己从六百万行Gecko代码中逃脱出来,进而产生一点点一览纵山小的感觉,以致对Gecko的认识有本质上的提升。现首先从XPCOM组件开始,因为它是Gecko的基础,就像COM对于windows,没有从根本上对XPCOM的理解与把握就不可能对Gecko有深入的理解,同时对XPCOM的理解也只是对Gecko理解的入门。

一、究竟什么是XPCOM?
XPCOM is a cross platform component object model. It has multiple language bindings, letting the XPCOM components be used and implemented in JavaScript, Java, and Python in addition to C++.它是一个组件对象模型,与Windows中的COM非常类似,如果以前深入接触过COM的朋友,应该对XPCOM的认识会很轻松,个人认为它相对COM来讲可能相对简单点,毕竟它不涉及到线程管理、内存管理,从大体来讲它仅仅包括最基本的对象生命周期管理模型,当中涉及到类工厂、Sington等模式的应用。可能由于XPCOM刚开始在90年代初设计的时候,对组件对象模型的认识还比较浅显,所以它采用了最基本的引用计数形式来管理组件对象,没有更深层次的与内存管理、线程管理结合起来,可能正是由于这个原因在Gecko的代码中经常看到相关的注释提醒作者注意这个变量是referenced,而不会真正的参与到真实对象的生命周期管理,以防止对象间循环引用,从而给可能的内存泄漏埋下了祸根。

二、为什么需要XPCOM?
也许这样问有人或许觉得很白痴。不过个人觉得如能真正理解为什么需要XPCOM,那么针对形式各异的XPCOM组件运用也就会见怪不怪啦。需要XPCOM这个模型个人认为应该是基于接口编程的真实需要,基于接口编程能够有效的实现对业务对象基于平台、基于语言及实现形式的扩展。Gecko作为一个跨平台的内核引擎,倘若没有这种基于接口的模型会使系统变得更加的庞杂,也许为了更加简单及统一Gecko已将XPCOM这一对象模型发挥引用得淋漓尽致,基本上能抽象独立出来的逻辑业务如Timer、Thread、EventQueue、http等等都通通定义成含有相对固定接口的对象模型。其实作为一个大型复杂系统,个人认为这样做是很有必要的。

现举两三例来说明,试想不同的平台如windows、linux等都有不同的图形界面接口,其中有的是原生的如xlib、windows原生接口,也有抽象层的如gtk、qt、wxwidget、mfc等,作为一个可以支持多种图形界面接口的内核,首先在结合自身需要及原生图形界面接口的基础之上抽象出诸如nsIBaseWindow、nsIWidget、nsIMenu、nsIEventSink等等公共的接口,然后针对不同平台的具体实现分布在mozilla\widget\src\不同的qt、windows、gtk2、mac目录中,这样直接明了统一。同时值得留意的是mozilla\widget\src\build\目录下有一个nsWinWidgetFactory.cpp,其主要职责在于根据编译选项选定的平台注册对应目录下实现的组件,以供其他组件调用。其主要内容为:
static const nsModuleComponentInfo components[] =
{
{ "nsWindow",
NS_WINDOW_CID,
"@mozilla.org/widgets/window/win;1",
nsWindowConstructor },
{ "Child nsWindow",
NS_CHILD_CID,
"@mozilla.org/widgets/child_window/win;1",
ChildWindowConstructor },
{ "Clipboard",
NS_CLIPBOARD_CID,
// "@mozilla.org/widget/clipboard/win;1",
"@mozilla.org/widget/clipboard;1",
nsClipboardConstructor },
{ "Clipboard Helper",
NS_CLIPBOARDHELPER_CID,
"@mozilla.org/widget/clipboardhelper;1",
nsClipboardHelperConstructor },
{ "AppShell",
NS_APPSHELL_CID,
"@mozilla.org/widget/appshell/win;1",
nsAppShellConstructor },
{ "Toolkit",
NS_TOOLKIT_CID,
"@mozilla.org/widget/toolkit/win;1",
nsToolkitConstructor },
{ "Look And Feel",
NS_LOOKANDFEEL_CID,
"@mozilla.org/widget/lookandfeel;1",
nsLookAndFeelConstructor },
{ "Transferable",
NS_TRANSFERABLE_CID,
// "@mozilla.org/widget/transferable/win;1",
"@mozilla.org/widget/transferable;1",
nsTransferableConstructor },
{ "HTML Format Converter",
NS_HTMLFORMATCONVERTER_CID,
"@mozilla.org/widget/htmlformatconverter;1",
nsHTMLFormatConverterConstructor },
};

NS_IMPL_NSGETMODULE(nsWidgetModule, components)

其实Gecko的实现代码中与此类似的有很多很多,如我们大家了解的MIMI类型有很多如html、xml、svg、txt、jpeg、gif、png等等,首先抽象出nsIDocument,然后依据其类型的不同分别实现其对应的逻辑,具体可参考\mozilla\content\base\public\nsIDocument.h;\mozilla\content\html\document\src\nsHTMLDocument.cpp,nsImageDocument.cpp;\mozilla\content\xul\document\src\nsXULDocument.cpp等;再如我们了解浏览器实现的协议有http、ftp等,为了统一不同协议的实现框架及能扩展到不同的协议,首先抽象出一组接口如nsIURL、nsIStreamListener、nsIStreamLoader、nsIChannel、nsIRequest、nsIRequesetObserver等,然后在不同的目录如http、ftp、file中按公共接口框架实现其逻辑,Gecko利用这种协议接口框架还扩展了about、data、resource等协议模式,如果了解了其公共接口框架并初步了解其中一个协议的实现方式,我们就可很方便的扩充到未实现的协议或自定义的协议实现。Gecko利用XPCOM模型为我们提供了很多很多类似的公共接口框架,以便扩展,其实总的说来,对于Gecko来讲这种方式几乎无处不在,正是这样的架构设计创造了Gecko的强大、丰富而具有生命力。有兴趣的朋友可以按这个基本思路从其代码实现中找到诸如扩展一个html标签、一个css属性、一种文件格式等等实现范例,以掌控其设计精髓以致扩展它为我们所用。

三、XPCOM主要特性
在XPCOM提供了丰富的功能的基础上,个人认为其主要特性有1、需要深入了解其引用计数对象管理机制及内存管理,特别在与跨语言方面如javascript、python等的实现与交互时,须防止跨边界的内存泄漏;2、很多组件通过xpconnect已实现与javascript的交互<其实这是Gecko非常重要的一部分,以后有机会再专题分析>,大大丰富了用c/c++实现的组件的应用场景,当然XPCOM组件也提供了与python、java方面的扩充与交互,不过支持程度没有javascript强,因为毕竟XPCOM组件与javascript的交互是Gecko的基石;3、为了充分利用组件对象模型,Gecko的实现中使用了大量的Sington、Observer、adapter等设计模式,如在nsDocShell中关于nsIWebProgress、AddProgressListener、nsIInterfaceRequestor等相关实现中得到充分的体现。其中Sington模式的使用有时往往具有鲜明的特点。其一般实现为XXXService,如WindowWatcher、CommandLineHandle、UriLoader等等。
///*****************************************************************************
// nsDocShell::nsIInterfaceRequestor
//*****************************************************************************
NS_IMETHODIMP nsDocShell::GetInterface(const nsIID & aIID, void **aSink)
{
NS_PRECONDITION(aSink, "null out param");

*aSink = nsnull;

if (aIID.Equals(NS_GET_IID(nsIURIContentListener))) {
*aSink = mContentListener;
}
else if (aIID.Equals(NS_GET_IID(nsIScriptGlobalObject)) &&
NS_SUCCEEDED(EnsureScriptEnvironment())) {
*aSink = mScriptGlobal;
}
else if ((aIID.Equals(NS_GET_IID(nsIDOMWindowInternal)) ||
aIID.Equals(NS_GET_IID(nsPIDOMWindow)) ||
aIID.Equals(NS_GET_IID(nsIDOMWindow))) &&
NS_SUCCEEDED(EnsureScriptEnvironment())) {
return mScriptGlobal->QueryInterface(aIID, aSink);
}
else if (aIID.Equals(NS_GET_IID(nsIDOMDocument)) &&
NS_SUCCEEDED(EnsureContentViewer())) {
mContentViewer->GetDOMDocument((nsIDOMDocument **) aSink);
return *aSink ? NS_OK : NS_NOINTERFACE;
}
else if (aIID.Equals(NS_GET_IID(nsIPrompt)) &&
NS_SUCCEEDED(EnsureScriptEnvironment())) {
nsresult rv;
nsCOMPtr wwatch =
do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);

nsCOMPtr window(do_QueryInterface(mScriptGlobal));

// Get the an auth prompter for our window so that the parenting
// of the dialogs works as it should when using tabs.

nsIPrompt *prompt;
rv = wwatch->GetNewPrompter(window, &prompt);
NS_ENSURE_SUCCESS(rv, rv);

*aSink = prompt;
return NS_OK;
}
else if (aIID.Equals(NS_GET_IID(nsIAuthPrompt))) {
return NS_SUCCEEDED(
GetAuthPrompt(PROMPT_NORMAL, (nsIAuthPrompt **) aSink)) ?
NS_OK : NS_NOINTERFACE;

}
else if (aIID.Equals(NS_GET_IID(nsISHistory))) {
nsCOMPtr shistory;
nsresult
rv =
GetSessionHistory(getter_AddRefs(shistory));
if (NS_SUCCEEDED(rv) && shistory) {
*aSink = shistory;
NS_ADDREF((nsISupports *) * aSink);
return NS_OK;
}
return NS_NOINTERFACE;
}
else if (aIID.Equals(NS_GET_IID(nsIWebBrowserFind))) {
nsresult rv = EnsureFind();
if (NS_FAILED(rv)) return rv;

*aSink = mFind;
NS_ADDREF((nsISupports*)*aSink);
return NS_OK;
}
else if (aIID.Equals(NS_GET_IID(nsIEditingSession)) && NS_SUCCEEDED(EnsureEditorData())) {
nsCOMPtr editingSession;
mEditorData->GetEditingSession(getter_AddRefs(editingSession));
if (editingSession)
{
*aSink = editingSession;
NS_ADDREF((nsISupports *)*aSink);
return NS_OK;
}

return NS_NOINTERFACE;
}
else if (aIID.Equals(NS_GET_IID(nsIClipboardDragDropHookList))
&& NS_SUCCEEDED(EnsureTransferableHookData())) {
*aSink = mTransferableHookData;
NS_ADDREF((nsISupports *)*aSink);
return NS_OK;
}
else if (aIID.Equals(NS_GET_IID(nsISelectionDisplay))) {
nsCOMPtr shell;
nsresult rv = GetPresShell(getter_AddRefs(shell));
if (NS_SUCCEEDED(rv) && shell)
return shell->QueryInterface(aIID,aSink);
}
else if (aIID.Equals(NS_GET_IID(nsIDocShellTreeOwner))) {
nsCOMPtr treeOwner;
nsresult rv = GetTreeOwner(getter_AddRefs(treeOwner));
if (NS_SUCCEEDED(rv) && treeOwner)
return treeOwner->QueryInterface(aIID, aSink);
}
else {
return nsDocLoader::GetInterface(aIID, aSink);
}

NS_IF_ADDREF(((nsISupports *) * aSink));
return *aSink ? NS_OK : NS_NOINTERFACE;
}

四、XPCOM相关参考资源

没有评论: