2008年7月29日星期二

浅谈Gecko关键部分之十XUL

Gecko内核作为一个功能强大的浏览器内核,其提出XUL(XML User Interface Language),并且由该语言来实现整个浏览器Firefox界面部分,XUL的实现从另个角度扩充了浏览器内核渲染引擎的内涵,因为它不仅仅可渲染HTML,同时可以象渲染HTML一样渲染xul,这样将XUL与HTML同等看待的浏览器内核实在独无仅有,从这个角度讲Gecko内核的渲染机制比IE、Webkit、Opera等浏览器内核要强大好多倍,因为它们目前本身仅仅支持HTML的渲染。虽然windows推出的XAML、adobe推出的Flex/Laszlo、yahoo widgets等都具有类似功能,但毕竟还没有与浏览器完全整合在一起。这样看来XUL作为Gecko内核的一部分具有根本而独特的作用,那就让我们抽时间来好好了解了解这个XUL。

一、什么是XUL?
XUL (XML User Interface Language) is Mozilla's XML-based language that lets you build feature-rich cross platform applications that can run connected or disconnected from the Internet. These applications are easily customized with alternative text, graphics and layout so they can be readily branded or localized for various markets.

二、XUL的实现机制
为了充分的达到利用XML+JS来实现图形界面程序,XUL作为一门类似HTML的语言,其中定义了一组标签、事件处理机制、渲染方式等,这些定义没有得到W3C等国际标准组织确认,仅由Mozilla组织定义并实现,解析处理一个xul文件的过程与处理html文件的主要过程是类似的,只不过在content、layout等方面有较大的不同,同时xul文件中可以嵌入html标签,将xul标签与html标签完全融为一体来处理,其主要处理过程完全类似浅谈Gecko关键部分之四The life of an html http Request中所描述的。

关于XUL具体定义的标签、CSS、事件处理等等,内容非常庞大,可具体参考下面提供的参考资源。

三、参考资源
WIKI XUL
Mozilla developer center-xul
XUL Reference
The Joy Of Xul
Xul Overlay
XUL Tutorial
XUL PLANET

WIKI XAML
WIKI Adobe Flex
WIKI Yahoo! Widget

2008年7月28日星期一

浅谈Gecko关键部分之九Plugin

Plugin作为一种对浏览器内核扩充的方式,为丰富浏览器应用提供了更加广阔的空间,如大家熟知的flash、java applet、window media player、vlc、adobe pdf等插件已经在人们的不知不觉中走到人们的生活当中,那么究竟什么是plugin?为firefox写的plugin能在其他浏览器中使用吗?如何为浏览器写plugin?浏览器实现plugin的机制及其基本原理如何?等等这些问题一直困扰在心头,现就这几方面来了解了解Gecko内核中的plugin实现等。

一、什么是plugin?
大家以前应该接触过一些关于plugin插件的概念,如eclipse就提供了很好的插件机制,以扩展ide的功能,apache也提供了丰富的插件机制,以丰富http server的处理请求。他们都有一个共同的特点就是以独立动态库的形式存在,动态库提供的输出接口应该满足主程序所规定的一些要求,以向主程序描述自身的特性及主要功能;而主程序也提供一组公共函数接口以供动态库调用,这样协同作业以完成任务。

在Gecko内核中plugin通过扩展html标签embed、object的MIMEtype类型的方式来拓展显示在一个页面中指定区域的内容。如大家熟知的application/x-shockwave-flash、application/x-java-applet、application/x-vlc-plugin等等,每一个plugin须说明自身所能支持的mime类型。一旦页面中遇到指定标签embed或object的mime类型,gecko内核就会搜索它目前所能支持的mime类型及对应的plugin,然后调用plugin中输出的接口由plugin来完成指定区域内的内容显示。

在Gecko内核中,就扩展的方向及方式的不同,plugin与xpcom拥有本质上的差别,plugin以扩展页面embed/object标签 mime类型的方式存在,往往与页面布局渲染相关,而xpcom是以扩展不同的组件种类,以实现不同内部功能的方式存在,它往往有更大的扩展空间包如字符串管理、线程管理、p2p传输管理等。他们的应用场景有本质上的差别,一般说来plugin相对内核而言更加独立如可以有独立的线程管理、内存管理、渲染方式等,而xpcom往往是内核中的一个组成部分。

Gecko内核plugin机制的实现接口Netscape Plugin Application Programming Interface
简称为NPAPI,它提供了相当完备的跨平台的接口框架,其中包括对Stream、Security、Scripting等方面的支持,目前包括Firefox、Opera、Safari、部分IE等浏览器都支持符合NPAPI接口标准的插件来扩展浏览器的功能。

二、如何编写plugin?
为了能编写plugin,首先需要了解其主要API,下面四个函数接口往往需要由plugin实现并export出去,可由外部即浏览器内核调用。
NPError WINAPI NP_Initialize(NPNetscapeFuncs* pFuncs);
当该plugin动态库第一次加载后浏览器内核会调用该方法,其中参数为当前内核提供给plugin调用的函数接口,plugin往往会将这些接口保存下来,以待以后使用;

NPError WINAPI NP_GetEntryPoints(NPPluginFuncs* pFuncs);
浏览器内核初始plugin动态库后,会调用该方法以获取该plugin实现的接口函数,内核会根据具体情况的不同来调用该plugin提供的不同接口函数;

NPError WINAPI NP_Shutdown();
当浏览器内核需要释放该plugin动态库时会调用该方法以告诉plugin其将会被释放;

char* NP_GetMIMEDescription(void);
浏览器内核通过该方法可获得该plugin支持的MIME类型,以便在解析页面embed/object标签时匹配其Type是否由哪个plugins来支持;

typedef struct _NPPluginFuncs {
uint16 size;
uint16 version;
NPP_NewUPP newp;
NPP_DestroyUPP destroy;
NPP_SetWindowUPP setwindow;
NPP_NewStreamUPP newstream;
NPP_DestroyStreamUPP destroystream;
NPP_StreamAsFileUPP asfile;
NPP_WriteReadyUPP writeready;
NPP_WriteUPP write;
NPP_PrintUPP print;
NPP_HandleEventUPP event;
NPP_URLNotifyUPP urlnotify;
JRIGlobalRef javaClass;
NPP_GetValueUPP getvalue;
NPP_SetValueUPP setvalue;
} NPPluginFuncs;

typedef struct _NPNetscapeFuncs {
uint16 size;
uint16 version;
NPN_GetURLUPP geturl;
NPN_PostURLUPP posturl;
NPN_RequestReadUPP requestread;
NPN_NewStreamUPP newstream;
NPN_WriteUPP write;
NPN_DestroyStreamUPP destroystream;
NPN_StatusUPP status;
NPN_UserAgentUPP uagent;
NPN_MemAllocUPP memalloc;
NPN_MemFreeUPP memfree;
NPN_MemFlushUPP memflush;
NPN_ReloadPluginsUPP reloadplugins;
NPN_GetJavaEnvUPP getJavaEnv;
NPN_GetJavaPeerUPP getJavaPeer;
NPN_GetURLNotifyUPP geturlnotify;
NPN_PostURLNotifyUPP posturlnotify;
NPN_GetValueUPP getvalue;
NPN_SetValueUPP setvalue;
NPN_InvalidateRectUPP invalidaterect;
NPN_InvalidateRegionUPP invalidateregion;
NPN_ForceRedrawUPP forceredraw;
} NPNetscapeFuncs;
NPPluginFuncs、 NPNetscapeFuncs结构中的函数接口分别由plugin和浏览器内核提供实现,而让对方调用。每个接口的具体含义及什么时候调用可参考下面提到的相关资源。

三、Gecko内核的plugin实现机制及基本原理
Gecko内核作为外部调用plugin的主程序,它不仅提供一组NPNetscapeFuncs函数接口给plugin调用,同时需要管理所有的plugin动态库、针对不同embed/object创建不同的plugininstance以及相关安全、创建窗口句柄、释放资源等。

在Gecko内核中提供了如下几个主要头文件、接口及类来完成相关任务。
\mozilla\modules\plugin\base\public\Npapi.h
\mozilla\modules\plugin\base\public\Npupp.h
\mozilla\modules\plugin\base\public\Npruntime.h
这几个头文件包含实现plugin所需要的函数接口、常量定义、数据结构定义等;

nsIPluginTag.idl
主要代表一个plugin动态库基本信息包括文件名、描述等;
nsIPluginTagInfo.idl/nsIPluginTagInfo2.idl
主要代表一个embed/object标签所描述的内容如MIME类型、参数、宽高等;
nsIPlugin.idl
主要描述一个plugin动态库所对应的initialize、shutdown、getMIMEDescription、getValue及创建plugin实例方法createPluginInstance,相当于plugin动态库加载后该plugin所提供的函数入口等;
nsIPluginInstance.idl
主要描述一个plugin动态库所实现的一个plugin实例所对应的函数接口,如initialize、start、stop、destroy、setWindow、newStream、getValue、handleEvent等;
nsIPluginHost.idl
主要描述如何装载plugin动态库、实例化EmbededPlugin、FullPagePlugin、检查指定MIME类型是否有对应plugin来实现等;

由类nsPluginHostImpl来代表"@mozilla.org/plugin/manager;1"、"@mozilla.org/plugin/host;1"来管理plugin等;它主要实现了接口nsIPluginHost、nsIPluginManager2等,提供了
void loadPlugins();
nsIPlugin getPluginFactory(in string aMimeType);
void instantiateEmbeddedPlugin(in string aMimeType, in nsIURI aURL, in nsIPluginInstanceOwner aOwner);
void setUpPluginInstance(in string aMimeType, in nsIURI aURL, in nsIPluginInstanceOwner aOwner);
void stopPluginInstance(in nsIPluginInstance aInstance);
等主要方法;

由类ns4xPlugini来实现nsIPlugin接口,其中维护了上面提到的函数接口
NPPluginFuncs fCallbacks;
PRLibrary* fLibrary;
NP_PLUGINSHUTDOWN fShutdownEntry;
/**
* The browser-side callbacks that a 4.x-style plugin calls.
*/
static NPNetscapeFuncs CALLBACKS;

在nsPluginHostImpl.cpp中通过static nsActivePluginList *gActivePluginList;来维护一组当前活动的plugin
class nsActivePluginList
{
public:
nsActivePlugin * mFirst;
nsActivePlugin * mLast;
PRInt32 mCount;
................................
};

struct nsActivePlugin
{
nsActivePlugin* mNext;
char* mURL;
nsIPluginInstancePeer* mPeer;
nsRefPtr mPluginTag;
nsIPluginInstance* mInstance;
PRTime mllStopTime;
PRPackedBool mStopped;
PRPackedBool mDefaultPlugin;
PRPackedBool mXPConnected;
//Array holding all opened stream listeners for this entry
nsCOMPtr mStreams;
.........................................
};

由nsPluginHostImpl、ns4xPlugin及nsActivePluginList等类能够维护好plugin动态库的加载、创建一个新plugin实例等,哪究竟是由谁来加载动态库、创建新的plugin实例呢?

作为一个html embed/object标签,其对应的元素为nsHTMLObjectElement,其继承自nsGenericHTMLFormElement、nsObjectLoadingContent、nsIDOMHTMLObjectElement,其中nsObjectLoadingContent提供了下面主要的方法
nsObjectLoadingContent::LoadObject(const nsAString& aURI,
PRBool aNotify, const nsCString& aTypeHint, PRBool aForceLoad)

nsresult nsObjectLoadingContent::TryInstantiate(const nsACString& aMIMEType,
nsIURI* aURI)

而nsHTMLObjectElement往往对应有nsObjectFrame;其中包含有 nsRefPtr mInstanceOwner;

而nsPluginInstanceOwner中含有如下元素:
nsPluginNativeWindow *mPluginWindow;
nsCOMPtr mInstance;
nsObjectFrame *mOwner;
nsCOMPtr mContent;
nsCString mDocumentBase;
char *mTagText;
nsCOMPtr mWidget;
nsCOMPtr mPluginTimer;
nsCOMPtr mPluginHost;

有了对基本的数据结构理解之后这样在parser html内容时,会根据embed/object标签创建nsHTMLObjectElement、nsObjectFrame、nsPluginInstanceOwner等实例,进而在nsHTMLObjectElement的LoadObject方法驱动下逐步的调用TryInstantiate、Instantiate、InstantiateEmbeddedPlugin、TrySetUpPluginInstance、Create4xPlugin等等,从而完成一个plugin实例的生成等。

至于其Scripting、Stream、security方面的支持须进一步参考nsIDOMClassInfo、nsGenericHtmlObjectSH及nsIPluginStreamListener等;

四、总结
通过初步了解plugin的实现机制后,然后自己动手参照Mozilla提供的plugin examples及相关API,完成一个基本的Plugin难度其实并不大,但是如想利用plugin技术实现如3D、Stream Media、叠加等方面的效果,须更进一步的了解3D、Stream Media等方面的内容。其实支持Gecko内核的Plugin相当多,如主流的播放器、flash、java applet、silverlight等等都有浏览器插件,再加上支持Gecko内核的plugin一般也支持Opera、Safari,这样如果我们掌握了NPAPI Plugin技术,那么我们就可以充分运用已有的plugin或自己动手创建一个好用的plugin,这样会大大的扩展浏览器的应用,让我们拭目以待吧。。。。。

五、参考资源
WIKI NPAPI
Mozilla projects -plugins
Mozilla developer center -plugins
Netscape Gecko Plugin API Reference
Mozilla Plugin Support on Microsoft Windows

2008年7月24日星期四

浅谈Gecko关键部分之八NSPR

Gecko内核作为一个跨平台的内核,具有很强的跨平台系统处理能力,其中就缺不了Netscape Portable Runtime (NSPR)的功劳,要想深入的了解Gecko内核,就不得不了解了解NSPR,它提供了基础性系统性功能的封装,在此基础上才有了线程管理、内存管理、I/O管理等等,下面提供一些资料以供参考学习。

一、什么是NSPR?
The Netscape Portable Runtime, or NSPR, is a platform abstraction library that makes all operating systems it supports appear the same to Mozilla. NSPR provides platform independence for non-GUI operating system facilities. These facilities include threads, thread synchronization, normal file and network I/O, interval timing and calendar time, basic memory management (malloc and free) and shared library linking.

相关参考资源
NSPR WiKi
Mozilla NSPR HomePage
NSPR Reference

二、为什么了解NSPR?
除了Gecko内核是基于NSPR的,其他一些应用也是基于NSPR;它具有很强的稳定性、可复用性等,了解NSPR,对了解Gecko内核是不可或缺的,就像想了解Apache Http Sever,就必须对APR有所了解。

了解了NSPR,除了让我们能更好更快更清晰的了解Gecko内核,其实还可以让我们从另一个侧面更好的了解不同操作系统的主要差异如线程的实现、线程同步的实现、I/O管理的不同等等,从而提升我们对操作系统的认识,进一步的把握计算机技术。

相关参考资源
Apache Portable Runtime(APR) WiKi
Simple DirectMedia Layer(SDL) WiKi

2008年7月20日星期日

浅谈Gecko关键部分之七安全及其实现

很长一段时间以来Firefox以安全、小巧、快捷著称,从而逐步从饱受不安全批评的IE中抢夺到部分用户的支持,在用户心中确立了Firefox比IE更安全的想法,其实浏览器的安全一直受到人们的广泛关注,但往往不能做到百分百的完美,似乎总有一些遗憾的地方,正所谓道高一尺魔高一丈。为了更深入的了解浏览器技术,安全正可谓不得不面对的问题,也许它比较的复杂,涉及面比较广,还是让我们从安全问题的出现、Gecko内核的安全策略及其主要实现路径等方面来学习研究,以便达到以一窥十的效果。

一、安全问题的出现
浏览器作为一个通往互联网的入口,其可以连接访问的网站成千上万,并且类型也缤纷多彩,正是这一点突出了浏览器功能强大、实用的特点,进而确立其在互联网应用中不可缺失的地位,但同时也带来了人们的疑虑,我们刚才访问的网站是否下载了病毒、是否破坏了操作系统、提交的数据如用户名密码是否泄漏、访问的历史记录是否遭到外部网站的截取等等。这些问题从浏览器出现的一刻起就受到浏览器开发者的关注,到目前为止,有些浏览器还是受到人们广泛的质疑。

其实困扰人们心中的浏览器安全问题往往可分为两类:
对用户本地私有数据的保护

私有数据往往包含两方面,一方面在于操作系统的文件是否遭到访问、读写等,其实一旦存在这类漏洞,其危害是特别大的,往往会导致整个操作系统的不安全,我们以前通过IE访问不良网站时,经常会遭遇到木马等病毒的骚扰,其实问题的出现正是由于病毒利用了activex组件等方面的漏洞,从而重新配置操作系统,并下载病毒文件等。另一方面在于保护用户保存的Cookie、历史访问记录,提交的表单数据是否遭到非法访问,在同一浏览中同时打开的不同页面是否能相互访问,共享数据等。

其实在浏览器设计之初就充分考虑到这些方面的问题,特别在javascript设计之初,因为浏览器能够执行外部网站的操作往往来源于javascript脚本,一旦能控制住浏览器为脚本提供的接口的安全性,浏览器的安全问题就得到架构上保证,再加上javascript本身是脚本语言,这样为任何一句脚本语言提供执行前的检查提供了可能。正是基于这样的出发点,javascript的实现是基于sanbox技术的,脚本语言的执行是在一个安全的上下文中进行的,它所造成的危害是可控的如从理论上讲,javascript是不可以访问操作系统文件的,是不可以上传不由用户指定的任意文件的。当然其中很多相关的安全细则可参考相关js手册,其实现是由内核来提供保证的。

对用户提交给网站数据的保护
浏览器往往通过http协议来与网站进行数据交互,由于http协议属于应用层协议,交互的数据对网络层来讲是透明的,也是不安全的,作为一个能广泛使用的应用程序,为了满洲需对提供的银行帐户与密码进行加密等需求,浏览器必须提供一些数据加密措施来保证数据交互的安全。基于这一点及广泛标准的支持,Gecko内核采用了PKI开源应用模块(相对来说蛮独立)来保证数据加密的要求如对PKCS #5, PKCS #7, PKCS #11, PKCS #12, S/MIME, TLS, SSL v2 and v3, X.509 v3 certificates等方面的支持,而没有使用目前很多项目都采用的OpenSSL来对SSL的支持,不过SSL作为一个标准,无论采取OpenSSL还是PKI,只不过是实现上的不同而已。

其中人们经常听说的反钓鱼技术,往往与此有点关联。
网络钓鱼 (phishing)是国际通用的新字,取phreak(偷接电话线的人)的前两个字母ph取代fishing(钓鱼)的f,是以社会工程学(也就是骗术)结合电脑科技的新犯罪手法。

网络钓鱼的目的是骗取受害人的网站上的帐号密码(网络银行或线上游戏)、信用卡资料以及个人资料,进行例如线上转帐、偷取游戏虚拟宝物、窥伺别人的email以及盗刷银行卡等违法行为。

钓鱼正以一种新的犯罪形式侵犯,在不断侵犯普通网民的合法利益,具体表现在伪装“网络银行”窃取用户的银行卡卡号和密码,转走用户在银行的存款;伪装成网 络游戏的官方网站窃取用户的网游帐号,转走用户在游戏里面的虚拟货币或者虚拟装备; 伪装成送QQ币的网站让用户输入QQ号和密码进而窃取QQ号……

其中Firefox应该是目前提供反钓鱼技术最好的浏览器,它不仅提供反盗取密码等网站,还提供反不安全、非法及含有黄色内容的网站。
其实现机制往往结合nsIContentPolicy接口及维护后台不安全网站列表来共同实现。

如须具体了解数据加密的内容,可更加深入的了解PKI OpenSource等方面的知识,这次主要学习研究对本地私有数据安全方面的内容。

二、Gecko内核的安全策略
为了保证数据的提交与行为的安全执行,Gecko内核中提供两种安全策略,Same-Origin Policy和Security Zones and Signed Scripts。

其中Same-Origin Policy的主要含义在于一段脚本只能访问具有同一来源的window及document的内容,同一来源包括同一主机、同一端口、同一协议,同时为了对扩展域名的支持,同一来源也指具有同一domain的不同window及document

而Security Zones and Signed Scripts指的是为了给一段脚本更高的访问权限,需要由用户确认后来授权该脚本可以拥有更高的权限。其实现的方式有通过SignTool及安全Jar的方式来授权、提示用户由用户来确认的方式来授权。其中前一种相当复杂,普通用户使用较少,而后一种方式应用较多,并且可以由用户自己来配置一些对象的属性、方法的访问权限或自定义一些域名浏览器访问权限,下面示例一些权限设置。
user_pref("capability.policy.default.Window.open", "noAccess");

user_pref("capability.policy.policynames", "strict");
user_pref("capability.policy.strict.sites", "http://www.evil.org http://www.annoying.com"); user_pref("capability.policy.strict.Window.alert", "noAccess");
user_pref("capability.policy.strict.Window.confirm", "noAccess");
user_pref("capability.policy.strict.Window.prompt", "noAccess");

user_pref("capability.principal.codebase.p2.granted", "UniversalXPConnect");
user_pref("capability.principal.codebase.p2.id", "http://192.168.1.200");
user_pref("capability.principal.codebase.p2.subjectName", "");


一段脚本可拥有的浏览器访问权限包括UniversalBrowserRead、UniversalBrowserWrite UniversalXPConnect、UniversalPreferencesRead、UniversalPreferencesWrite CapabilityPreferencesAccess、UniversalFileRead等,它们可以通过netscape.security.PrivilegeManager.enablePrivilege("UniversalPreferencesRead");等方式来设置权限,如果用户的signed.applets.codebase_principal_support配置选项设为true,在设置权限时会提示用户是否设置该codebase权限。

三、安全主要实现路径
首先由javascript SpiderMonkey提供了一组安全方面的接口,以供脚本安全的检查,至于具体的安全规则则由xpconnect及securitymanager来实现,主要接口有JSPrincials、JS_CheckAccess、
JSObjectOps.checkAccess、JSClass.checkAccess、JS_SetPrincipalsTranscoder等;

然后由nsScriptSecurityManager来管理维护其提供的nsPricipal实例及相关的安全检查,其中在其初始化时完成如下主要操作:

nsresult nsScriptSecurityManager::Init()
{
nsresult rv = InitPrefs();//将用户自定义的pricipal组织起来
NS_ENSURE_SUCCESS(rv, rv);
。。。。。。。。。。。。。。。。
// Create our system principal singleton
nsRefPtr system = new nsSystemPrincipal();
NS_ENSURE_TRUE(system, NS_ERROR_OUT_OF_MEMORY);
rv = system->Init();
NS_ENSURE_SUCCESS(rv, rv);

mSystemPrincipal = system;
。。。。。。。。。。。。。。。
//-- Register security check callback in the JS engine
// Currently this is used to control access to function.caller
nsCOMPtr runtimeService =
do_GetService("@mozilla.org/js/xpc/RuntimeService;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);

rv = runtimeService->GetRuntime(&sRuntime);
NS_ENSURE_SUCCESS(rv, rv);

//CheckObjectAccess成为安全检查的主要入口点
JS_SetCheckObjectAccessCallback(sRuntime, CheckObjectAccess);

sXPConnect->GetXPCWrappedNativeJSClassInfo(&sXPCWrappedNativeJSClass,
&sXPCWrappedNativeGetObjOps1,
&sXPCWrappedNativeGetObjOps2);

return NS_OK;
}

在实现的过程中会提供一些nsPrincipal实例如nsNullPrincipal、nsSystemPrincipal来表示不同的安全规则,同时每一个nsPricipal实例都包含一个JSPricipals接口实例,其内容可由JS_SetPrincipalsTranscoder来根据不同情况来设定,这样可以为不同的JSContext、JSClass、JSObject、JSFunction设置对应的JSPricipals,由它可以获得nsPrincipal实例,以了解真正的安全规则。

其中由SpiderMonkey JS Engineer内部或Xpconnect、nsScriptSecurityManager等外部在需要实施安全规则检查的地方如检查xpcom组件接口参数发起JS_CheckAccess的执行,其中会调用上面提到的回调函数CheckObjectAccess,以触发nsSecurityManager来统一对安全的检查,起到sanbox的作用。具体的nsPricipal及nsSecurityManager的管理可参考\mozilla\caps\src\nsPricipal.cpp及nsScriptSecurityManger.cpp等,至于如何为JS对象设置一些JSPricipals则需要更深入对函数JS_SetPrincipalsTranscoder的调用与实现作一些了解。

四、参考资源
Mozilla Security Review and Best Practices Guide
The Same Origin Policy
Signed Scripts in Mozilla
Configurable Security Policies
JSAPI User Guide
Open Source PKI Projects



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

2008年7月7日星期一

浅谈Gecko关键部分之五认识HTML、DOM、CSS、XML、XHTML、SVG等标准

在了解Gecko内核是如何响应用户输入的Web地址,如何渲染、显示包含文字、图片、动画甚至视频的画面给用户的整个过程之后,假如我们深思一下,Gecko内核为什么要这样解析、渲染这个Web地址对应的内容?为什么不那样解析?它在解析、渲染的时候会受到约束吗?它可以自由发挥吗?答案是肯定,它所受的约束也就是我们熟知的W3C标准,它是尽可能按照标准来办事的,当然标准当中也肯定有未尽之事,所以它也有一定的自由发挥空间。正是有了如此众多的W3C标准,让我们的互联网应用是如此的丰富多彩,IE、Safari、Opera等浏览器都基本按照标准办事,所以我们编写的符合标准的Web页面可以在如此多的浏览器上被解析、渲染显示出来,并且效果基本一致。标准也就是协议,所以我们要想深入的了解Gecko内核,必须深入的了解它所支持的标准。下面初略的介绍一下HTML、DOM、CSS、XML、XHTML、SVG等标准。

一、HTML
HTML, an initialism of HyperText Markup Language, is the predominant markup language for web pages. It provides a means to describe the structure of text-based information in a document — by denoting certain text as links, headings, paragraphs, lists, and so on — and to supplement that text with interactive forms, embedded images, and other objects. HTML is written in the form of tags, surrounded by angle brackets. HTML can also describe, to some degree, the appearance and semantics of a document, and can include embedded scripting language code (such as JavaScript) which can affect the behavior of Web browsers and other HTML processors.

具体可参考
HTML Wiki
HTML 4.01 Specification
HTML 5 Specification

二、
DOM

The Document Object Model (DOM) is a platform- and language-independent standard object model for representing HTML or XML and related formats.

A web browser is not obliged to use DOM in order to render an HTML document. However, the DOM is required by JavaScript scripts that wish to inspect or modify a web page dynamically. In other words, the Document Object Model is the way JavaScript sees its containing HTML page and browser state.

具体可参考
DOM Wiki
DOM2 Html Specification

三、CSS
Cascading Style Sheets (CSS) is a stylesheet language used to describe the presentation of a document written in a markup language. Its most common application is to style web pages written in HTML and XHTML, but the language can be applied to any kind of XML document, including SVG and XUL.

具体可参考
CSS Wiki
CSS Specification

四、XML
The Extensible Markup Language (XML) is a general-purpose specification for creating custom markup languages.[1] It is classified as an extensible language because it allows its users to define their own elements. Its primary purpose is to facilitate the sharing of structured data across different information systems, particularly via the Internet,[2] and it is used both to encode documents and to serialize data. In the latter context, it is comparable with other text-based serialization languages such as JSON and YAML.[3]

具体可参考
XML Wiki
XML Specification

五、XHTML
The Extensible Hypertext Markup Language, or XHTML, is a markup language that has the same depth of expression as HTML, but also conforms to XML syntax.

具体可参考
XHTML Wiki
XHTML Specification

六、SVG
Scalable Vector Graphics (SVG) is an XML specification and file format for describing two-dimensional vector graphics, both static and animated. SVG can be purely declarative or may include scripting. Images can contain hyperlinks using outbound simple XLinks.[2] It is an open standard created by the W3C's SVG Working Group.

具体可参考
SVG Wiki
SVG Tiny1.2 Specification

七、总结
面对如此众多的W3C标准,特别是W3C上发布的标准文档,我们往往会看得云里雾里,毕竟这些标准文档是所谓的专家学者所写,够严谨全面的。为了便于日常运用,我们只要把握两条即可1、懂得其基本用途及主要要素; 2、必要时刻需仔细研读之,定会收获众多,她们就像我们的良师益友,平时没事的时候可常习之。




2008年7月5日星期六

浅谈Gecko关键部分之四The life of an html http Request

从用户的角度来看,一个浏览器其实是相当简单的,用户输入一个Web地址,然后按回车,等不了多久,一个具有丰富内容的画面就会出现在用户面前,其中包含五彩缤纷的排列整齐的文字、图片、动画甚至视频,用户通过点击自己感兴趣的地方,新的内容画面就会呈现在用户面前,这样用户就可以只需简单的点击一下鼠标就可以在互联网上自由遨游啦。现在回想起来,也许正是浏览器当初在设计的时候充分考虑到用户的易用性才导致互联网的蓬勃发展,正像谷歌提供搜索一样简单易用并且非常实用,这样说来,互联网的发展离不开浏览器,是她带领人们走向了互联网,回顾历史让我们感慨万千,那么浏览器主要做了些啥?她为什么能支持如此丰富的内容?她的发展方向在哪里?为了解答心中的疑问,首先让我们初步了解Gecko内核是如何完成从用户输入Web地址,直到内容丰富的画面显示出来的整个过程,这个过程涉及的面比较广,目前我们主要关心关键过程、关键点及相关的关键类的实现,争取能有个整体的而又完整的印象。

一、创建nsWebShellWindow实例,准备处理输入的WebURI地址;
因为接收用户打开一个新URI的情况比较多,如通过脚本中的window.open或直接在地址栏中输入或者在新开的一个窗口打开或者在新建的一个页签打开等等,通过对这一系列的打开一个页面的外围场景进行处理后,内核都会新建一个或利用原有的nsWebShellWindow实例来处理整个打开URI的过程。其包含的主要成员有nsComPtr mWindow,代表一个原生窗口,将要打开的页面可在其中显示内容如文字、图片等;nsComPtr mDocShell,代表一个协调者,它管理整个打开URI的过程,其实现类nsDocShell也就成为整个Gecko内核的核心部分,它不仅接收处理打开URI请求,同时提供接口以反馈相关信息给外部如提示正在打开某某地址等。在nsWebShellWindow的Initialize()方法中通过mWindow = do_CreateInstance(kWindowCID, &rv);来创建mWindow,通过mDocShell = do_CreateInstance("@mozilla.org/webshell;1");来创建设置mDocShell为打开URI做好前期准备。

二、准备装载指定URI的内容;
首先由协调者nsDocShell的LoadURI方法来发起装载的请求,经过一番安全或请求Policy等方面的设定后创建nsCOMPtr uriLoader=do_GetService(NS_URI_LOADER_CONTRACTID, &rv);
nsCOMPtr channel;
rv = NS_NewChannel(getter_AddRefs(channel),
aURI, nsnull, nsnull, static_cast(this), loadFlags);
由uriLoader->OpenURI(aChannel, (mLoadType == LOAD_LINK), this);来统一继续处理所有的URI请求,其中会创建一个临时的nsCOMPtr loader =new nsDocumentOpenInfo(aWindowContext, aFlags, this);同时将loader作为一个nsIStreamListener来接收请求而来的数据,以分析请求而来的文档类型如text/html、xml等以决定后续不同的处理,然后由rv = channel->AsyncOpen(loader, nsnull);来真正向Web服务发出请求,并通过异步的方式来处理接收数据,这里往往会涉及到以前提到的线程处理等,总之至此基本的准备动作应该都已准备好,就等待Web服务端返回数据啦。

三、根据接收的数据类型创建Document及相关ContentViewer;
根据异步数据的获取,获取数据的线程向MainThread发出nsInputStreamReadyEvent事件,由主线程来处理相关事件,其往往会调用到nsInputStreamPump中去,其关键调用如下:
nsInputStreamPump::OnInputStreamReady(nsIAsyncInputStream *stream)
{
LOG(("nsInputStreamPump::OnInputStreamReady [this=%x]\n", this));

// this function has been called from a PLEvent, so we can safely call
// any listener or progress sink methods directly from here.

for (;;) {
if (mSuspendCount || mState == STATE_IDLE) {
mWaiting = PR_FALSE;
break;
}

PRUint32 nextState;
switch (mState) {
case STATE_START:
nextState = OnStateStart();
break;
case STATE_TRANSFER:
nextState = OnStateTransfer();
break;
case STATE_STOP:
nextState = OnStateStop();
break;
}
...........

mState = nextState;
}
return NS_OK;
}

当STATE_START时,表示已获取部分
初始数据,其中OnStateStart()会调用作为Channel的streamListener的OnStartRequest方法,此时streamListener为uriLoader临时创建的nsDocumentOpenInfo实例loader,由它来处理,其方法OnStartRequest进而由nsDocumentOpenInfo的DispatchContent方法来尝试决定能否处理该文档类型,一旦决定可以处理该类型,进而由nsDSURIContentListener的DoContent来调用nsDocShell的方法CreateContentViewer来创建Document及Viewer,其中主要进行代码包括:
{
..........................................
// Instantiate the content viewer object
nsCOMPtr viewer;
nsresult rv = NewContentViewerObj(aContentType, request, mLoadGroup,
aContentHandler, getter_AddRefs(viewer));

NS_ENSURE_SUCCESS(Embed(viewer, "", (nsISupports *) nsnull),
NS_ERROR_FAILURE);
............................................
}
nsresult
nsDocShell::NewContentViewerObj(const char *aContentType,
nsIRequest * request, nsILoadGroup * aLoadGroup,
nsIStreamListener ** aContentHandler,
nsIContentViewer ** aViewer)
{
nsCOMPtr aOpenedChannel = do_QueryInterface(request);

nsresult rv;
nsCOMPtr catMan(do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv));
if (NS_FAILED(rv))
return rv;

nsXPIDLCString contractId;
rv = catMan->GetCategoryEntry("Gecko-Content-Viewers", aContentType, getter_Copies(contractId));

// Create an instance of the document-loader-factory
nsCOMPtr docLoaderFactory;
if (NS_SUCCEEDED(rv))
docLoaderFactory = do_GetService(contractId.get());

if (!docLoaderFactory) {
return NS_ERROR_FAILURE;
}

// Now create an instance of the content viewer
// nsLayoutDLF makes the determination if it should be a "view-source" instead of "view"
NS_ENSURE_SUCCESS(docLoaderFactory->CreateInstance("view",
aOpenedChannel,
aLoadGroup, aContentType,
static_cast(this),
nsnull,
aContentHandler,
aViewer),
NS_ERROR_FAILURE);

(*aViewer)->SetContainer(static_cast(this));
return NS_OK;
}

这里主要会根据contentType的不同创建不同的Document对象,它由nsContentDLF::CreateInstance来实现。其主要代码如下

nsCOMPtr doc;
nsCOMPtr docv;
do {
// Create the document
doc = do_CreateInstance(aDocumentCID, &rv);//针对htmldocumnt、xuldocument、imagedocument的不同会对应不同的DocumnetCID
if (NS_FAILED(rv))
break;

// Create the document viewer XXX: could reuse document viewer here!
rv = NS_NewDocumentViewer(getter_AddRefs(docv));
if (NS_FAILED(rv))
break;
docv->SetUAStyleSheet(gUAStyleSheet);

doc->SetContainer(aContainer);

// Initialize the document to begin loading the data. An
// nsIStreamListener connected to the parser is returned in
// aDocListener.
rv = doc->StartDocumentLoad(aCommand, aChannel, aLoadGroup, aContainer, aDocListener, PR_TRUE);
if (NS_FAILED(rv))
break;

// Bind the document to the Content Viewer
rv = docv->LoadStart(doc);
*aDocViewer = docv;
NS_IF_ADDREF(*aDocViewer);
} while (PR_FALSE);

其中需要注意的是在StartDocumentLoad的时候会创建一个mParser = do_CreateInstance(kCParserCID, &rv);作为DocListener,以供后面解析文档内容用;同时会创建nsCOMPtr sink,以供后面组织文档内容用;创建ContentSink的主要代码逻辑如下:
// create the content sink
nsCOMPtr sink;

if (aSink)
sink = aSink;
else {
if (IsXHTML()) {
nsCOMPtr xmlsink;
rv = NS_NewXMLContentSink(getter_AddRefs(xmlsink), this, uri,
docShell, aChannel);

sink = xmlsink;
} else {
nsCOMPtr htmlsink;

rv = NS_NewHTMLContentSink(getter_AddRefs(htmlsink), this, uri,
docShell, aChannel);

sink = htmlsink;
}
NS_ENSURE_SUCCESS(rv, rv);

NS_ASSERTION(sink,
"null sink with successful result from factory method");
}

mParser->SetContentSink(sink);
// parser the content of the URI
mParser->Parse(uri, nsnull, (void *)this);
}

一旦初步组织好nsDocShell、Document、Viewer、Parser、ContentSink之间的关系后,在nsDocumentOpenInfo调用DispatchContent时将返回的DocListener也即nsParser实例赋值给nsDocumentOpenInfo的m_targetStreamListener,以供后续使用,同时告诉Parser、ContentSink作好解析、组织文档准备;

另外通过nsDocShell的Embed方法,初始化DocumentViewer,其主要实现如下:
nsDocShell::SetupNewViewer(nsIContentViewer * aNewViewer)
{
..........................
mContentViewer = aNewViewer;
nsCOMPtr widget;
NS_ENSURE_SUCCESS(GetMainWidget(getter_AddRefs(widget)), NS_ERROR_FAILURE);

nsCOMPtr deviceContext;
if (widget) {
deviceContext = do_CreateInstance(kDeviceContextCID);
NS_ENSURE_TRUE(deviceContext, NS_ERROR_FAILURE);
deviceContext->Init(widget->GetNativeData(NS_NATIVE_WIDGET));
}

nsRect bounds(x, y, cx, cy);

mContentViewer->Init(widget, deviceContext, bounds);
..........................
}

DocumentViewerImpl::InitInternal(nsIWidget* aParentWidget,
nsISupports *aState,
nsIDeviceContext* aDeviceContext,
const nsRect& aBounds,
PRBool aDoCreation,
PRBool aInPrintPreview,
PRBool aNeedMakeCX /*= PR_TRUE*/)
{
.......................................
if (aParentWidget && !mPresContext) {
// Create presentation context
if (mIsPageMode) {
//Presentation context already created in SetPageMode which is calling this method
}
else
mPresContext =
new nsPresContext(mDocument, nsPresContext::eContext_Galley);
NS_ENSURE_TRUE(mPresContext, NS_ERROR_OUT_OF_MEMORY);

nsresult rv = mPresContext->Init(aDeviceContext);
if (NS_FAILED(rv)) {
mPresContext = nsnull;
return rv;
}

if (aDoCreation && mPresContext) {
// The ViewManager and Root View was created above (in
// MakeWindow())...

rv = InitPresentationStuff(!makeCX, !makeCX);
}
................................................
}

DocumentViewerImpl::InitPresentationStuff(PRBool aDoInitialReflow, PRBool aReenableRefresh)
{
..............................................

// Create the style set...
nsStyleSet *styleSet;
nsresult rv = CreateStyleSet(mDocument, &styleSet);
NS_ENSURE_SUCCESS(rv, rv);

// Now make the shell for the document
rv = mDocument->CreateShell(mPresContext, mViewManager, styleSet,
getter_AddRefs(mPresShell));

mPresShell->BeginObservingDocument();//mDocument->AddObserver(mPresShell);
..............................................

}

nsDocument::doCreateShell(nsPresContext* aContext,
nsIViewManager* aViewManager, nsStyleSet* aStyleSet,
nsCompatibility aCompatMode,
nsIPresShell** aInstancePtrResult)
{
*aInstancePtrResult = nsnull;

NS_ENSURE_FALSE(mShellsAreHidden, NS_ERROR_FAILURE);

FillStyleSet(aStyleSet);

nsCOMPtr shell;
nsresult rv = NS_NewPresShell(getter_AddRefs(shell));
if (NS_FAILED(rv)) {
return rv;
}

rv = shell->Init(this, aContext, aViewManager, aStyleSet, aCompatMode);
NS_ENSURE_SUCCESS(rv, rv);

// Note: we don't hold a ref to the shell (it holds a ref to us)
NS_ENSURE_TRUE(mPresShells.AppendElementUnlessExists(shell),
NS_ERROR_OUT_OF_MEMORY);
shell.swap(*aInstancePtrResult);

return NS_OK;
}
这样同时准备好与显示相关的nsDeviceContext、nsPresContext、nsPresShell,其中需要留意的是nsPresShell对象是作为nsDocument
实例对象的一个Observer,同时也是nsViewManager实例对象的一个Observer;至此初始化完nsPresShell,并加入到nsViewManager的管理机制中去,为后面创建新的nsView对象及进行布局安排、显示内容作好准备。

四、解析、组织文档内容,同时作好布局
准备
继续查看nsInputStreamPump::OnInputStreamReady(nsIAsyncInputStream *stream)方法中的STATE_TRANSFER时,表示已获取相关文档数据,此时会继续利用uriLoader临时创建的nsDocumentOpenInfo,由它的方法OnDataAvailable来继续处理获得的数据,其实现如下:

NS_IMETHODIMP nsDocumentOpenInfo::OnDataAvailable(nsIRequest *request, nsISupports * aCtxt, nsIInputStream * inStr, PRUint32 sourceOffset, PRUint32 count)
{
// if we have retarged to the end stream listener, then forward the call....
// otherwise, don't do anything
nsresult rv = NS_OK;
if (m_targetStreamListener)
rv = m_targetStreamListener->OnDataAvailable(request, aCtxt, inStr, sourceOffset, count);
return rv;
}
此时其
m_targetStreamListener值也即上面创建出来的nsParser实例,
这样正好一环套一环,任务转交给了上面准备好的nsParser;
Gecko内核中的Parser有HtmlParser以解析html文档;xmlParser以解析符合xml格式的文档如xul、svg;CssParser以解析Css文档等。解析器通过Tokenizer来认识文档中的标识,然后结合不同文档类型预定义的Atom及ContentSink来组织想要的文档结构。整个HtmlParser的解析过程还是蛮复杂的,不过无论解析的过程怎样,其结果就是会适时的调用ContentSink中的主要方法,如OpenHead、OpenBody、CloseBody、OpenForm、CloseForm、OpenContainer、CloseContainer、StartLayout、ProcessLINKTag、ProcessSCRIPTEndTag、ProcessSTYLEEndTag等;

其中需要留意的是在构建文档结构的同时往往需要对不同的Content Node 创建对应Frame对象,同时针对不同的Frame,还时可能还需要构建nsView,如nsBoxFrame,在一些条件下需要调用方法CreateViewForFrame来创建一个nsView实例,其主要代码如下:
{
// Create a view
nsIView *view = viewManager->CreateView(aFrame->GetRect(), parentView, visibility);
if (view) {
// Insert the view into the view hierarchy. If the parent view is a
// scrolling view we need to do this differently
nsIScrollableView* scrollingView = parentView->ToScrollableView();
if (scrollingView) {
scrollingView->SetScrolledView(view);
} else {
viewManager->SetViewZIndex(view, autoZIndex, zIndex);
// XXX put view last in document order until we can do better
viewManager->InsertChild(parentView, view, nsnull, PR_TRUE);
}
}

// Remember our view
aFrame->SetView(view);
}

NS_IMETHODIMP_(nsIView *)
nsViewManager::CreateView(const nsRect& aBounds,
const nsIView* aParent,
nsViewVisibility aVisibilityFlag)
{
nsView *v = new nsView(this, aVisibilityFlag);
if (v) {
v->SetPosition(aBounds.x, aBounds.y);
nsRect dim(0, 0, aBounds.width, aBounds.height);
v->SetDimensions(dim, PR_FALSE);
v->SetParent(static_cast(const_cast(aParent)));
}
return v;
}

同时对于一些nsView还需要通过其nsIView::CreateWidget方法来创建原生的窗口,主要代码如下

nsresult nsIView::CreateWidget(const nsIID &aWindowIID,
nsWidgetInitData *aWidgetInitData,
nsNativeWidget aNative,
PRBool aEnableDragDrop,
PRBool aResetVisibility,
nsContentType aContentType,
nsIWidget* aParentWidget)
{
nsView* v = static_cast(this);

v->LoadWidget(aWindowIID))
..................................
mWindow->Create(aNative, trect, ::HandleEvent, dx, nsnull, nsnull, aWidgetInitData);
..............................
}
在mWindow->Create中会接着调用如nsWindow::StandardWindowCreate、BaseCreate等;
其中
参数::HandleEvent会在创建原生窗口时保存在nsWindow的属性成员mEventCallback中;这个函数句柄在原生窗口的处理函数中经过一定处理后往往根据一定上下文而去调用它,它会处理如WM_SIZE、WM_PAINT等主要消息,后面的处理会引用到。

五、显示文档内容
在上面解析、组织文档结构时,当调用OpenBody时会适时调用StartLayout,其主要代码如下
{
nsPresShellIterator iter(mDocument);
nsCOMPtr shell;
while ((shell = iter.GetNextShell())) {
// Make sure we don't call InitialReflow() for a shell that has
// already called it. This can happen when the layout frame for
// an iframe is constructed *between* the Embed() call for the
// docshell in the iframe, and the content sink's call to OpenBody().
// (Bug 153815)

PRBool didInitialReflow = PR_FALSE;
shell->GetDidInitialReflow(&didInitialReflow);
if (didInitialReflow) {
// XXX: The assumption here is that if something already
// called InitialReflow() on this shell, it also did some of
// the setup below, so we do nothing and just move on to the
// next shell in the list.
continue;
}

nsRect r = shell->GetPresContext()->GetVisibleArea();
nsCOMPtr shellGrip = shell;
nsresult rv = shell->InitialReflow(r.width, r.height);

}

由nsPresShell的InitialReflow方法来启动布局,主要作用是根据文档元素的类型、属性的不同,而决定是否需要重新布局该文档元素及其子元素,一旦觉得有必要重新布局则调用nsPresShell的PostReflowEvent方法,它通过向主线程MainThread发送一个ReflowEvent,一旦MainThread接收到该ReflowEvent,其会由对应nsPresShell的DoFlushPendingNotifications方法来处理,其最终根据当前的nsView及原生窗口的不同,在windows平台上它会对原生的窗口句柄进行InvalidateRect或UpdateWindow处理,按照windows图形管理的逻辑,系统会根据相关条件,及时向该原生窗口句柄发送原生的WM_PAINT消息,而对处理原生的窗口消息,一般会由窗口创建时提供的窗口回调函数来处理,经过一番处理判断后会触发调用nsWindow::DispatchWindowEvent,进而会调用上面提到的
mEventCallback所对应的公共的函数,其实现如下:
//
// Main events handler
//
nsEventStatus PR_CALLBACK HandleEvent(nsGUIEvent *aEvent)
{
//printf(" %d %d %d (%d,%d) \n", aEvent->widget, aEvent->widgetSupports,
// aEvent->message, aEvent->point.x, aEvent->point.y);
nsEventStatus result = nsEventStatus_eIgnore;
nsView *view = nsView::GetViewFor(aEvent->widget);

if (view)
{
view->GetViewManager()->DispatchEvent(aEvent, &result);
}

return result;
}
进而由nsViewManager::DispatchEvent统一来处理事件,这时经过一些处理,往往需要重新渲染相关View,而渲染View的主要代码如下:

void nsViewManager::RenderViews(nsView *aView, nsIRenderingContext& aRC,
const nsRegion& aRegion)
{
if (mObserver) {
nsView* displayRoot = GetDisplayRootFor(aView);
nsPoint offsetToRoot = aView->GetOffsetTo(displayRoot);
nsRegion damageRegion(aRegion);
damageRegion.MoveBy(offsetToRoot);

aRC.PushState();
aRC.Translate(-offsetToRoot.x, -offsetToRoot.y);
mObserver->Paint(displayRoot, &aRC, damageRegion);
aRC.PopState();
}
}
而上面提到的nsPresShell实例往往作为nsViewManager的一个Observer,这样工作就这样转交给nsPresShell来处理。而nsPresShell的Paint方法的主要内容如下:

PresShell::Paint(nsIView* aView,
nsIRenderingContext* aRenderingContext,
const nsRegion& aDirtyRegion)
{
AUTO_LAYOUT_PHASE_ENTRY_POINT(GetPresContext(), Paint);
nsIFrame* frame;
nsresult rv = NS_OK;

if (mIsDestroying) {
NS_ASSERTION(PR_FALSE, "A paint message was dispatched to a destroyed PresShell");
return NS_OK;
}

NS_ASSERTION(!(nsnull == aView), "null view");

frame = static_cast(aView->GetClientData());
nscolor backgroundColor;
mViewManager->GetDefaultBackgroundColor(&backgroundColor);
for (nsIView *view = aView; view; view = view->GetParent()) {
if (view->HasWidget()) {
PRBool widgetIsTransparent;
view->GetWidget()->GetHasTransparentBackground(widgetIsTransparent);
if (widgetIsTransparent) {
backgroundColor = NS_RGBA(0,0,0,0);
break;
}
}
}

if (!frame) {
if (NS_GET_A(backgroundColor) > 0) {
aRenderingContext->SetColor(backgroundColor);
aRenderingContext->FillRect(aDirtyRegion.GetBounds());
}
return NS_OK;
}

nsLayoutUtils::PaintFrame(aRenderingContext, frame, aDirtyRegion,
backgroundColor);

return rv;
}

相关主要工作又转交给nsLayoutUtils::PaintFrame;其主要工作就是通过 nsDisplayListBuilder builder来建立一个nsDisplayList list,并调用nsDisplayList::Paint方法,其实现如下:

void nsDisplayList::Paint(nsDisplayListBuilder* aBuilder, nsIRenderingContext* aCtx,
const nsRect& aDirtyRect) const {
for (nsDisplayItem* i = GetBottom(); i != nsnull; i = i->GetAbove()) {
i->Paint(aBuilder, aCtx, aDirtyRect);
}
nsCSSRendering::DidPaint();
}

在其中的for循环中会根据DisplayList Item构成的不同,会调用不同frame的Paint方法等,如
nsDisplayText::Paint(nsDisplayListBuilder* aBuilder,
nsIRenderingContext* aCtx, const nsRect& aDirtyRect) {
static_cast(mFrame)->
PaintText(aCtx, aBuilder->ToReferenceFrame(mFrame), aDirtyRect);
}
通过这样一个流程,就会显示出相关内容给用户。


六、总结分析
通过上面初步的整理,对整个过程有了初步的了解,其中关键在于了解到LoadURI的发起、nsIStreamListener数据的接收、nsParser及nsContentSink对文档的组织构成、nsPresShell结合nsFrame、nsView、nsViewManager、nsWindow对内容的显示。总的说来整个过程是相当的复杂,毕竟自从Web服务端获取数据后在内核中需要不断的动态创建很多
对应的类对象,但关键流程的流转在于对nsInputStreamReadyEvent事件和ReflowEvent两个事件的处理,有了这样基本的认识之后,对于css文档、image文档、js文档、plugin的处理,应该有了扎实的基础准备,至于具体每一html标签、xul标签具体对应哪些Node、Frame、Widget还须具体结合自身的不同加以分析。

七、参考网址
The Life Of An HTML HTTP Request