2008年7月13日星期日

浅谈Gecko关键部分之六认识javascript实现及应用

作为一个目前应用范围非常广的脚本语言javascript,或许每一个程序员都听说过并使用过,但据部分统计,许多程序员对javascript的认识存在一定误解,认为它非常简单,会写javascript简直算不了什么,并且认为它在Web开发中的地位根本没有如php、ruby等开发语言及所谓的框架那么实用好用,以致没有重视javascript在Web开发中所应有的作用。为寻求Gecko内核究竟是如何实现javascript以及它在内核中的地位究竟如何,乃至进一步探讨一下javascript的发展方向,下面分别就什么是Javascript、它在Gecko中的实现及其扩展、应用等方面作一些学习研究总结,以致从整体上对javascript有更清晰的认识。

一、什么是javascript?
javascript是一门动态、弱类型、基于原型的脚本语言,其核心部分包含对脚本的编译、解析、执行、反编译、垃圾回收及标准类/类型如 bool、string、number的实现等;然后在此基础通过将已经符合DOM标准的文档DOM接口按照javascript接口定义及声明的方式绑定/导出到 javascript运行环境中,以丰富javascript的内涵,其本质相当于有了java虚拟机后,通过import不同类型的类库来扩展 java语言的应用。

目前javascript比较独特的地方在于它自身没有一个完整独立的运行环境,其往往依附于浏览器,由浏览器来提供运行环境,并控制或发起javascript进行编译、解析执行脚本、垃圾回收等,其核心部分相当于一个符合ECMAScript标准的动态库,以供浏览器来调用,这样看来其本质是为了对浏览器主要部分的扩展及更灵活运用的支持,从MVC的角度来看,javascript相当于浏览器中的控制部分,其应用场景往往具有一定的局限性、独特性的;而通用的脚本语言如perl、python、 ruby等都提供了完整独立的语言运行环境及线程管理等,并且提供了良好的扩展机制以丰富语言的内涵。

在Gecko内核中xpconnect扮演为javascript提供运行环境及扩展的角色,其不仅将符合DOM标准的接口扩展到javascript运行环境中去,同时将大部分xpcom接口也扩展到整个javascript运行环境中,这一点在其他浏览器如Safari、IE是没有的,这样就奠定了 javascript在整个Gecko内核中不仅可访问文档的DOM接口,同时可以访问极大部分xpcom接口,进而可以使用javascript编写 xpcom组件,以供其他xpcom组件访问。从架构的角度看Gecko内核对javascript的扩展提供了极大的支持,但由于Gecko内核中有一部分重要组件是由javascript来实现的,从而绑定javascript成为Gecko内核的核心部分,虽然这样给程序的开发带来了极大的方便性,但也带来了调试等方面的不便。

二、javascript在Gecko中的实现及其扩展
为了更好的扩展及模块化,Mozilla对 javascript的实现是以SpiderMonkey+xpconnect的方式来实现的,其中SpiderMonkey主要是按照 ECMAscript的方式来实现javascript语言的核心部分,其提供了一组公共JSAPI以供外部调用,SpiderMonkey是相当独立的一部分,它可以单独编译,可以嵌入到其他程序中去,下一个版本可能会整合Tamarin相关的内容;

而xpconnect作为一个xpcom服务类组件,具有sington特性,从javascript 语言的角度来看,它为javascript提供了运行环境,同时按照SpiderMonkey提供的JSAPI对其进行了扩展;从xpcom角度来看,xpconnect将很大一部分xpcom接口以javascript的接口方式绑定出去,以便其能在javascript的运行环境中以 javascript的方式来引用xpcom组件及接口。其复杂程度是相当大的,因为它既要按照ECMAScript标准的要求考虑到脚本语言方面的控制如动态性、安全方面等,同时它要尽可能将xpcom组件方面的接口绑定出去给javascript脚本使用,并且两者的内存管理、对象生命周期的管理机制是完全不同的,再加上javascript及xpcom本身具有一定的动态特性。因此,xpconnect部分也就成为内存泄漏及效率提升方面的重点关注对象。

SpiderMonkey按照ECMAScript标准提供的主要外部接口有:
Starting up相关JS_NewRuntime、JS_NewContext、JS_NewObject、JS_InitStandardClasses
Defining objects and properties相关JS_DefineObject、JS_DefineProperties
Defining functions and classes相关JS_DefineFunctions、JS_InitClass
Execute/Call function相关JS_EvaluateScript、JS_ExecuteScript、JS_CallFunctionName
Shutting down相关JS_DestroyContext、JS_DestroyRuntime、JS_ShutDown
其具体实现及应用可以参考后面提供的网址。

xpconnect is a technology which enables simple interoperation between XPCOM and JavaScript.
其主要内容有
将用c/c++实现的xpcom组件接口(包含标准的DOM、CSS接口等)绑定出去给javascript运行环境使用。
为了统一方便使用,Gecko内核提出供一组nsDOMClassInfo子类,如nsWindowSH、nsLocationSH、 nsElementSH、nsDocumentSH等,作为Script Helper类,他们实现了接口nsIXPCScriptable及nsIClassInfo。

每当xpconnect需要将原生c/c++xpcom组件对象绑定给javascript使用时,它可通过 InitClassesWithNewWrappedGlobal或WrapNative方法来处理,但都会调用到 XPCConvert::NativeInterface2JSObject方法,这个方法主要处理过程是:
首先获取该组件对象对应的nsDOMClassInfo子类实例,整个Gecko内核拥有很多Script Helper nsDOMClassInfo子类实例(其往往在初始化时存储在一个Hash表中,并建好相关查询索引);

然后利用获取的Script Helper nsDOMClassInfo子类实例的PreCreate方法来创建该组件对应的jsObject,同时会创建XPCWrappedNative类实例,在其中保存好mScriptableInfo信息,其如nsCOMPtr mCallback往往为nsDOMClassInfo子类实例,

如需要会为其创建prototype原型对象以便后面共享,这时需要向javascript运行环境提供一组回调API,其主要函数在 mozilla\js\src\xpconnect\Xpcwrappednativejsops.cpp中如 XPC_WN_Helper_AddProperty、XPC_WN_Helper_DelProperty、 XPC_WN_Helper_NewResolve等,通过观测这些回调函数发现他们往往首先会通过传递过来的jsObject获取 XPCWrappedNative对象,然后获取其ScriptableInfo,根据相关flag来决定是否调用Script Helper 实例中对应的方法,而Script Helper实例往往在初始时就已经知道原生绑定出去的xpcom组件能支持的一些xpcom接口,进而可以对原先不认识的接口、属性、方法、参数,进行 QueryInferface,进行WrapNative处理,将结果返回给js使用。

其中需要注意的是有些组件或类对象没有直接实现nsIClassInfo,哪么nsCOMPtr info =do_QueryInterface(aComObj);是如何获得Script Helper nsIClassInfo 实例呢?

费了九牛二虎之力,终于发现Gecko内核中使用了一些诡异的办法来到达目的的。示例如下:

定义一个Macro
#define NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(_class) \
if (aIID.Equals(NS_GET_IID(nsIClassInfo))) { \
foundInterface = NS_GetDOMClassInfoInstance(eDOMClassInfo_##_class##_id); \
if (!foundInterface) { \
*aInstancePtr = nsnull; \
return NS_ERROR_OUT_OF_MEMORY; \
} \
} else

而extern nsIClassInfo* NS_GetDOMClassInfoInstance(nsDOMClassInfoID aID);就是根据ID到sClassInfoData数组中去匹配即可,这个数组在初始化时就填充完毕,其不同ID值对应不同的 nsIClassInfo 实例如nsWindowSH、nsDocumentSH实例等。

而在\mozilla\dom\src\base\nsGlobalWindow.cpp中通过
// QueryInterface implementation for nsGlobalChromeWindow
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(nsGlobalChromeWindow)
NS_INTERFACE_MAP_ENTRY(nsIDOMChromeWindow)
NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(ChromeWindow)
NS_INTERFACE_MAP_END_INHERITING(nsGlobalWindow)
来设定do_QueryInterface时对nsIClassInfo的支持。
另外还有一些类也是通过该方式来对nsIClassInfo的支持。

三、javascript在Gecko中的应用
在Gecko内核及Firefox本身的实现中使用了很多javascript代码,其中不仅仅是对基本javascript语言及DOM 接口的使用,同时充分利用了上面提到到xpcom绑定出去的组件接口,假如对xpcom及其绑定原理不是很清楚,也就对Firefox当中许多js看不懂,往往让人纳闷Gecko内核中的js为什么会多出这么多非ECMAScript所定义的关键字如Components、interfaces、classes等,其具体含义是什么?虽然XULPlant.org对其中有一定的说明,但还是显得抽象,不具体。在有了上面最基本的认识后,如需全面掌握Gecko内核究竟向javascript绑定导出了多少接口、属性及方法等,可具体参考文档\mozilla\dom\src\base\nsDOMClassInfo.cpp的nsDOMClassInfo::Init()方法中一大段关于nsIDOMClassInfo方面的初始化。如
DOM_CLASSINFO_MAP_BEGIN(Window, nsIDOMWindow)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMWindow)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMJSWindow)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMWindowInternal)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMEventTarget)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMViewCSS)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMAbstractView)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMStorageWindow)
DOM_CLASSINFO_MAP_END

DOM_CLASSINFO_MAP_BEGIN(Location, nsIDOMLocation)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMLocation)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMNSLocation)
DOM_CLASSINFO_MAP_END

DOM_CLASSINFO_MAP_BEGIN(XMLHttpRequest, nsIXMLHttpRequest)
DOM_CLASSINFO_MAP_ENTRY(nsIXMLHttpRequest)
DOM_CLASSINFO_MAP_ENTRY(nsIJSXMLHttpRequest)
DOM_CLASSINFO_MAP_ENTRY(nsIDOMEventTarget)
DOM_CLASSINFO_MAP_ENTRY(nsIInterfaceRequestor)
DOM_CLASSINFO_MAP_END

这样就对Firefox browser.js中的类似下面的代码就不觉得迷糊啦。
const nsCI = Components.interfaces;
const nsIWebNavigation = nsCI.nsIWebNavigation;
window.XULBrowserWindow = new nsBrowserStatusHandler();
window.QueryInterface(nsCI.nsIInterfaceRequestor)
.getInterface(nsIWebNavigation)
.QueryInterface(nsCI.nsIDocShellTreeItem).treeOwner
.QueryInterface(nsCI.nsIInterfaceRequestor)
.getInterface(nsCI.nsIXULWindow)
.XULBrowserWindow = window.XULBrowserWindow;
window.QueryInterface(nsCI.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();

四、总结
经过上面的学习研究,觉得不管是javascript基本语法及应用方面,还是其具体实现方面,还有xpconnect实现方面等,都可单独写一本书来描述,其内容实在太多,并且比较复杂,以后可多花点心力来继续了解他们。但是为了更好的了解其实现过程,能充分发挥javascript优势,不妨多用用DOM Inspecter及javascript debuger venkman等工具。

五、参考资源
Wiki Script language
WiKi Javascript
Core Javascript1.5 Guide
XPConnect(Scriptable Components)
The Mozilla DOM Hacking Guide
JavaScript-DOM Prototypes in Mozilla
JSAPI User Guide
SpiderMonkey Internals
SpiderMonkey Thread Safety
SpiderMonkey Garbage Collection Tips
How to embed the javascript engine
Tamarin