对于插件的定义,早期的有微软的ActiveX和网景的NPAPI插件,随后,Chromium项目考虑到性能引入了PPAPI插件机制,同时为了安全方面的考虑,引入Native Client机制,这些插件机制扩展了游览器的能力,极大的丰富了网页的应用场景,同时,随着HTML5的发展很多HTML5功能同样需要扩展JavaScript的编程接口,以便开发者可以使用JavaScript代码来调用,而这样的机制需要相应的机制来支持。

NPAPI插件

NPAPI简介

NPAPI全称是网景插件应用程序编程接口,最早是有网景公司提出,用于让游览器执行外部程序,以支持网页中各种格式的文件,对于有些网络资源或者文件,游览器本身不支持它们,但是,经过第三方开发者开发的插件程序,游览器可以做到支持。在Chromium中使用插件的方法很简单,在网页中声明如下语句即可,它表示使用上述插件来打开一个PDF文件并显示在网页中:

<embed id="plugin" type="application/pdf" src="src/T.pdf">

NPAPI提供两组接口,一类以NPP开始,由插件来实现,被游览器调用,主要包括一些插件创建、初始化、关闭、销毁、信息查询及事件处理、数据流、窗口设置、URL等,另一类以NPN开始,由游览器来实现,被插件所调用,主要宝库图形绘制、数据流处理、游览器信息查询、内存分配和释放、游览器的插件设置、URL等,这两类能够满足大多数双方的需求。

WebKit和Chromium的实现
WebKit基础设施

NPAPI插件获得了WebKit的支持,因为它的广泛实用性,在HTML网页中,可以通过两种类型的元素“embed”和“object”来使用插件,两者都可以用来在网页中内嵌插件,下图给出了WebKit中支持插件机制所使用的类及其结构,初看起来比较复杂和杂乱无章,以下分左、中、右三个部分分别介绍它们:

javascript扩展硬件设备 javascript 扩展插件_html5


左边部分表示插件元素在DOM树和RenderObject树中的节点类,因为有两种HTML元素可以表示插件,所以为它们抽象出来了一个基类,对于插件元素在DOM树中的对应节点,RenderObject树中对应就是RenderWidget对象,用于表示这是个可视化的元素,在某些WebKit移植中,甚至引入了硬件加速机制来加速插件的绘制,例如WebKit的Qt移植,它的基本思想是将插件元素作为单独的一个层来处理,插件的实例将绘制所有内容在这一层上,就像视频元素一样。

图中右侧部分表示的是WebKit如何管理插件库,主要使用两个类:

  • PluginDatabase:注册和管理所有的插件实现,一个插件通常是一个动态库,插件的信息包括名字、描述、版本,还有最重要的MIME类型和文件的扩展名,一个插件可以支持多种类型的文件,同时,它能够根据MIME类型和文件扩展名来查找相应的插件库。
  • PluginPacket:表示一个插件库,也就是PluginDatabase类管理的对象,它包含两个非常重要的变量,就是m_pluginFuncs和m_browserFuncs,对应的就是NPP开头的函数组和NPN开头的函数组

在中间部分是插件的视图部分,和DOM元素或者RenderWidget对象一一对应,其作用是绘制插件的可视化结果,同时需要调用最后测的类来获取插件。

  • NPP:使用PluginPackage的接口来创建的插件实例
  • PluginViewBase:抽象类,主要是定义一些接口,这些接口会被HTMLPluginElement类调用,用来处理视图方面的一些操作,如鼠标、聚焦等
  • PluginView:表示的是一个插件的视图,它连接了插件库和网页中DOM接口和可视化RenderObject节点,包含所需的插件库和插件实例
  • NPObject:表示的是插件和游览器之间数据的交互类型,因为插件能够访问DOM树和JavaScript对象,所以JavaScript中的基本类型和JavaScript对象都会包装成NPObject来在两者之间传递

对于插件机制,有一下几点需要注意:
1 插件库管理机制:关机的基础是MIME类型和文件扩展名,例如对于“”这样的例子,PluginView类会将“.pdf”文件扩展民当作参数传递给PluginDatabase并期待返回一个PluginPackage对象,对于某个MIME类型,当出现多个插件支持的时候,管理机制需要决定如何选择它们
2 插件节点的处理,在网页中出现“embed”和“object”元素的时候,WebKit会首先创建HTMLPluginElement对象,之后需要创建RenderWidget节点,当出现硬件加速机制的时候,可能还需要创建相应的RenderLayer节点,同时还要创建PluginView对象,并根据DOM元素的属性来查找并创建相应的实例
3 绘图工作,本身NPAPI没有提供绘图的接口,只是让插件将绘制完的结构传给游览器或者提供一个绘制的目标存储结构,从而让哈建直接在它上面绘制,这就是插件的Window和Windowless模式。

虽然插件机制支持“object”或者“embed”元素,但是,该机制也能够扩展JavaScript中国对象和对象的方法,例如希望在JavaScript中增加W3C组织定义的一些标准接口,如设备相关的对象和方法。在WebKit的插件设计架构中,渲染引擎同插件运行通常在同一进程中,这个设计将会带来稳定性和安全性方面的灾难性后果,为了避免这些方面的问题,Chromium在WebKit/Blink插件架构的基础上引入了跨进程的插件机制,这为游览器的稳定性提供了保证,同时考虑到性能方面的问题,Google提出了新的PPAPI插件机制,考虑到安全性和支持本地代码的问题,引入了Native Client机制,为安全性提供了保证。

Chromium插件架构

为了解决插件的稳定性问题,同时由于Chromium的沙箱模型机制,插件实例不能够在Render进程中运行,因为除了访问IO之外,没有访问其他接口和资源的能力,所以在Chromium中插件是被单独的进程中来执行的,这就是Chromium的插件多进程模型,下图显示了插件进程示例图:

javascript扩展硬件设备 javascript 扩展插件_扩展_02


在Chromium中,每一个插件库只会有一个进程,也就是说如果有两个或者多个Render进程同时使用同一个插件库,那么这些Renderer进程会共享同一个插件进程,因为多个Renderer进程共享同一种的Plugin进程,在Chromium中加载插件库后为每个插件使用点在plugin进程中创建一个对应的插件实例,插件进程最终由Browser进程来负责创建和销毁,而不是Render进程,原因是Render进程没有创建的权限,而且Plugin进程也应该由Browser进程来统一管理,这样更方面,当Plugin进程创建成功时,Browser进程会返回进程间通信的句柄,用于创建和Plugin进程通讯的PluginChannelHost,当没有任何插件实例并且空闲一段时间后,plugin进程才会被销毁,这样做的好处是避免频繁的创建plugin进程。下图描述了Browser进程和Plugin进程间的通信机制及其所涉及的相关的模块,Browser进程通过PluginProcessHost发送消息调用Plugin进程的函数,相应动作由PluginThread完成,而Plugin进程则是通过WebPluginProxy发送消息调用Browser进程的响应函数,响应动作由PluginProcessHost完成:

javascript扩展硬件设备 javascript 扩展插件_扩展_03


Browser进程和Plugin进程仅有较少的消息传递,用于插件的创建等管理工作,其实主要的工作在Render进程和Plugin进程之间,机制也相对复杂一些,HTMLPluginElement节点是DOM树中的一个节点,在Chromium的实现中会包含一个WebPluginContainerImpl,该节点是WebKit::Widget的子类,也就是Chromium中的一个对PluginView的具体实现类,而它包含一个WebPluginImpl,对plugin的调用有WebPluginDelegateProxy负责中转,在Plugin进程中,由WebPluginDelegateStub处理所有Render进程发送过来的请求,并由WebPluginDelegateImpl调用创建好的PluginInstance对象,PluginInstance最终调用PluginLib读取的插件库的各个函数入口地址,最终完成对插件库实现的调用,而对插件实现中对NPN开头函数的调用,则是通过PluginHost来完成,PluginHost主要负责实现NPN开头的函数,这些函数被Plugin进程所调用,可以在Plugin和Render进程被调用,当在plugin进程调用这些函数时,chromium会覆盖PluginHost的部分函数,而这些新的callback函数会调用NPObjectProxy来通过IPC发送请求到Render进程,PluginInstance实现了NPP开头的函数,被Render进程所调用,PluginInstance通过PluginLib获得了插件库中这些函数的地址,从而把实际的调用桥接到具体的插件中:

javascript扩展硬件设备 javascript 扩展插件_html5_04

对NPObject相关的函数调用有专门的类来处理,NPObject的调用或者访问是双向的,它们的具体实现是通过NPObjectProxy和NPObjectStub来完成,NPObjectProxy接收来自对方的访问请求,转发给NPObjectStub,最后NPObjectStub调用真正的NPObject并返回结果。

Chromium插件的工作过程

插件工作过程主要是创建并完成插件和游览器的交互过程,下图给出一个插件如何被Render进程触发创建的过程:

javascript扩展硬件设备 javascript 扩展插件_html5_05


如果页面中包含一个“embed”或者“object”元素,Render进程会创建一个HTMLEmbedElement元素,当该元素被JavaScript代码或者其他地方使用的时候,会触发创建相应的插件,HTMLEmbedElement对象会请求创建自己对应的RenderWidget,进而创建WebPluginImpl和WebPluginDelegate-Proxy,如果该插件的进程不存在,WebPluginDelegateProxy会发送消息到Browser进程,请求该进程来创建Plugin进程,Plugin进程被Browser进程创建后,会响应Render进程的请求来创建PluginInstance并将它初始化,这样它们之间的联系就建立好了。

Window和Windowless插件

根据规范,可以通过设置“embed”或者“object”元素的属性来让游览器来决定如果提供绘制结果的存储方式,Window模式插件有Render进程提供一个窗口,插件直接在该窗口上进行绘制,所以它不需要和网页的内容在进行合并,而是一个独立的绘制目标,而Windowless模式的插件则不同,插件将绘制的结果如Pixmap通过共享内存的方式传递给Render进程,Render进程然后绘制该内容到自己内部的存储结构上,因此可以看出Window模式的性能是要高于Windowless的,但是对于Window模式的插件来说,它不能跟网页的内部内容构成很好的前后关系,而对于Windowless模式的插件来说,性能较差的问题带来的好处是可以把插件绘制的结构和网页上的其他内同做各种形式的合成。

Chromium PPAPI插件

原理

插件是一种统称,表示一些动态库,这些动态库根据定义的一些标准接口可以跟游览器进行交互,PPAPI的提出是因为NPAPI的可移植性和性能存在比较大的问题,特别是针对跨进程的插件,同时还有插件需要2D和3D绘图、声音等问题时候更为棘手。在现在的NPAPI插件系统中,通常的做法是当网页需要显示插件的时候或者需要更新的时候它会发送一个失效(Invalidate)的通知,让插件来绘制它们,而在PPAPI插件机制中,它引入了一个保留(Retained)模式,其含义是游览器始终保留一个后端存储空间,用来表示上一次绘制完的区域,因为PPAPI插件通常是跨进程的,因此游览器可以绘制网页而不需要锁,与此同时插件进程能够在后台绘制新的结果。PPAPI插件有两种运行模块,受信(Trusted)插件和非受信(Untrusted)插件,对于受信的PPAPI插件可以在Render进程中运行,也可以在另外的进程中运行,对于新版本的实现,架构设计都是基于IPC来设计的,对于非受信的PPAPI插件,可以借助于使用NativeClient技术来安全运行,受信插件是与平台相关的,可以调用平台相关的接口,而对于非受信插件而言,它们可以是与平台无关的代码,可以调用NativeClient提供的有限接口,而不能调用其他接口。PPAPI插件同样使用“embed”或者“object”元素,这让网页看起来没什么大的区别,因此对于WebKit而言,不会区分背后的是NPAPI还是PPAPI,差别在于调用的接口不一样而已。

接口

同NPAPI的NPN和NPP开头的接口相似,PPAPI也需要双向调用的编程接口,PPAPI提供了游览器调用插件的接口,同时更是提供了众多插件调用游览器各种功能的接口,这些接口的标准定义文件位于ppapi/api中,它们都使用一种接口定义语言(IDL)来描述,IDL是一种标准,其中以ppb_(ppapi browser)开头的接口文件表示这是由游览器实现,被插件库所调用,以ppp_(ppapi plugin)开头的接口文件表示这是由插件实现,被游览器调用,以pp_开头的接口尾巴尖表示共享的接口定义,两边都需要使用,主要是一些基础类定义等,不同于NPAPI只是提供C接口,PPAPI既提供了C接口,同时又提供了C++接口,C接口主要是函数指针和结构为主,而C++接口则是提供各种作用的类。公共部分的接口包括各个基础数据,如时间、大小、矩形和资源,这些类会作为后面定义接口的参数来传递,对应的接口例如PP_Time,PP_Size,PP_Rect和PP_Resource,这里最为重要的接口是PP_Resource,它表示各种类型的资源,例如文件资源、音频资源、图像资源、图形资源等。由插件实现的接口大致包括以下几个部分:第一部分是插件模块和插件实例,用于初始化和关闭插件的管理插件功能的接口,例如 PPP_InitializeModule(),PPP_ShutdownModule(),而插件的实例类,表示一个插件的实例对象,也就是InterfacePPP_Instance,这里面包含多个函数,如DidCreate、DidDestory等,表示当创建插件之后,游览器调用它们,以便插件能够做一些后续的辅助工作,第二部分是一些事件的通知接口,表示游览器需要派发一些消息给插件,典型的包括鼠标事件、通用消息传递接口、3D图形上下文丢失事件和鼠标锁定事件等,由游览器实现的接口,主要提供各种能力给插件使用,这其中包括2D和3D图形绘制接口、文件IO、文件系统、鼠标事件、网络、游戏手柄、时间等,这些都是PPAPI机制中定义的可以被游览器调用的资源及其编程接口。

工作过程
基础设施

对于插件模块和实例接口,有插件进程直接调用并根据需要加载和创建它们,在PPAPI插件机制中,复杂的是资源的调用,也就是游览器提供给插件使用的各种资源接口,下图描述了跨进程模式下PPAPI插件机制中资源是如何被插件调用的,如前面一样,PPAPI的插件是在插件进程中被加载的,当它需要使用插件的时候,通过图中的Thunk设施将C接口转成C++接口来调用相应的PluginResource类,该类是所有资源的基类,是一个代理类,负责发送请求给其他进程,拥有接口其他进程发过来的调用结果的能力,发送请求有相应的其它类来帮助,这里是PluginDispatcher类和HostDispatcher类,它们都会使用IPC::Channel来发送消息,消息会被Browser进程和Renderer进程中的BrowserPpapiHost类和RenderPpapiHost类处理,以调用真正的实现函数,其中这两个进程都有ResourceHost的子类,这是因为某些资源的实现在Render进程完成,例如2D和3D图形资源,但是有些类必须在Browser进程中处理,如文件和文件系统等:

javascript扩展硬件设备 javascript 扩展插件_扩展_06

工作过程

以Chromium中PPAPI的2D绘图例子来做说明PPAPI插件的工作过程,分两个部分,第一个是HTML网页,第二个是实现插件的文件,下图是使用插件的网页代码:

<html>
<head>
  <title>2D Example</title>
</head>
<body>
  <embed id="plugin" type="application/x-ppapi-example-2d">
</body>
</html>

在WebKit中,对于PPAPI和NPAPI的支持都是类似的,二者都是根据MIME类型来查找PPAPI插件机制,如果Chromium发现查找到的是一个PPAPI插件而不是NPAPI插件,那么在创建WebPluginContainerImpl对象的时候,就会首先创建一个WebPlugin子类的对象,这里不是一个WebPluginImpl对象,而是一个PepperWebPluginImpl对象,之后它就发送消息到插件进程,请求创建一个插件的实例,回到插件进程,它会根据插件的注册信息查找需要的插件并调用它的构造函数来初始化该插件的模块,如下面的CreateModule方法,之后需要调用该方法的返回对象来创建一个插件实例的对象,如CreateInstance方法,会创建一个插件类自定义的一个示例:

class SubModule : public pp::Module{
public:
  virtual pp::Instance* CreateInstance(PP_Instance instance) {
    return new SubInstance(instance);
  }
};
namespace pp{
  Module* CreateModule(){
    return new SubModule();
  }
}

当SubInstance被创建的时候,Chromium会创建PPP_Proxy_Instance对象,该对象接收从Render进程发送过来的关于该实例的状态信息,如插件视图改变、销毁等,然后在调用插件的相应接口,这些接口是在插件中实现并有游览器调用的。对于资源的创建,一个插件实例可能会用到多个资源,如绘图资源、文件资源等,下面的OnPaint函数使用到了2D绘图资源,由于它使用了PaintManager类,当需要更新视图的时候,该类需要创建一个Graphics2D资源对象:

class SubInstance:public pp::Instance, public pp::PaintManager::Client{
public:
  SubInstance(PP_Instance instance){...}
  virtual bool HandleInputEvent(...){...}
  virtual void OnPaint(pp::Graphics2D& graphics_2d,...){...}
private:
  pp::PaintManager paint_manager_;
};

为了详细说明它的调用过程,下图描述了资源类对象的创建和资源类对象接口的调用过程,分别以插件进程和Render进程的交互为例,而插件进程和Browser进程的交互则是类似的:

javascript扩展硬件设备 javascript 扩展插件_扩展_07


上图中包括两个步骤,第一个是在插件进程中完成,第二个步骤在Render进程中完成,当PPAPI插件需要创建一个资源对象的时候,会通过PPAPI的C接口调用Chromium内部的实现,Thunk层将其转换成C++风格的调用,在插件进程中会有一个工厂类来创建不同类型的资源对象,本例中Graphics2DResource对象在创建的同时会发送一个消息到Render进程,这是第二个步骤,Render进程同样包含一个能够创建不同类型ResourceHost对象的工程类,以帮助完成资源对象的创建。

javascript扩展硬件设备 javascript 扩展插件_插件_08


上图描述了当资源对象创建完之后,插件需要调用资源对象的接口来完成特定的操作,这个过程包含三个步骤,其发生在两个进程中,首先,还是插件进程接收到插件的调用请求,并把请求发送给Render进程,其次Render进程接收响应,然后执行特定的操作,并将结果值返回或者通知插件进程该动作执行完成,最后是插件进程接收到返回值或者动作执行完的消息,如果需要,它还可以调用插件的函数来通知插件,当然,某些调用不需要从Render进程返回结果到插件进程,因此前两步是必须的,而第三步则是可选的。

Native Client
基本原理

NativeClient简称NaCl,是一种沙箱技术,能偶提供给平台无关的不受信本地代码一个安全的运行环境,可以针对那些计算密集型的需求,例如游戏引擎、可视化计算、大数据分析、3D图形渲染等,这些场合只需要访问有限的一些本地接口,不需要通过网络服务来计算,以免占用额外的带宽资源,同时,它能够比较方便的将原来使用系统语言例如c++编写的库直接移植到Web平台中,它同WebGL、WebAudio这样的技术所解决的问题相似,但是途经不同,因为这些技术是规范,而NativeClient及时是Google提出的,使用NativeClient能够将很多本地库的能力轻易的提供给网页使用,而不需要复杂的移植过程,给重用带来很大的方便。本身PPAPI和NativeClient没有必然联系,两者解决的是不同方面的问题:PPAPI提供弄个插件机制;NativeClient使用PPAPI的插件机制将使用NativeClient技术编译出来的本地库运行同游览器交互起来,目前NativeClient是基于PPAPI接口来实现的,其实之前NativeClient也曾经基于NPAPI接口来实现,所以能够在Firefox、Safari和Opera游览器中运行,目前已经不能了。

NaCl本质上是一个运行环境,该子系统提供了很少的一些受限系统调用接口和资源的抽象,本地库只能调用它们而不能任意使用系统调用,与沙箱模型的不同在于,NaCl是将一个第三方开发的代码库运行在受限的环境中,而沙箱模型是将一个进程运行在首先环境中,NaCl提供编译工具,将使用C/C++代码编写的代码编译生成它能运行的可执行格式-nexe,本地代码调用的也都是本地接口,同JavaScript的交互都是NaCl机制和PPAPI机制来完成。下面给出相关的架构图来帮助了解:

javascript扩展硬件设备 javascript 扩展插件_html5_09


首先Render进程如果没有后面的部分,NaCl插件和其他PPAPI插件没有什么特别的差别,同时通过PPAPI跟渲染引擎进行通信,但是这里NaCl插件只是一个桥接工作,它将同游览器(渲染引擎)的交互交接到使用NaCl及时的本地可执行库“nexe”。然后Chromium会创建一个新的进程,该进程使用过了沙箱技术,只能访问特定的系统接口,这样限定了该进程中的任何库都不能超越它们,在Render进程中的NaCl插件使用消息传递机制同sel_ldr进程通信,在消息机制之上是一种称为SPRC(简单远程调用技术),该机制可以实现PPAPI的跨进程调用,不过目前插件同NaCl的实现之间的通信机制SRPC已经不支持,都是通过一个新的接口PostMessage来实现,该接口意味这都是通过消息机制来进行的。最后,sel_ldr提供的环境能够运行nexe,nexe是不受心的部分,该机制可以用一个沙箱来将它运行在限定的sel_ldr进程中,nexe没有办法使用NaCl提供的接口之外的系统接口,从而保证了安全,nexe是本地代码库,所以跟平台相关,例如32位和64位习哦他能够,需要两份不同的代码库。

pNaCl

nexe需要不同的版本,一个关键的问题在于NaCl提供的编译工作只能将NaCl的是想爱你直接编译成同硬件结构相关的本地代码,所以不同的平台需要生成不同的本地库,pNaCl提供了一套新的工作,该工具能够将C/C++代码编译成LLVM字节码,LLVM能够将C/C++代码转成字节码,该字节码是平台无关的,而且该字节码可以保存起来,当字节码在某个平台上运行的时候,LLVM的后端能够根据字节码生成特定平台的本地代码。

JavaScript引擎的扩展机制

混合编程

由于游览器能力的不足,特别是以前的游览器甚至不支持内嵌视频和音频等技术,导致需要Flash等插件来扩展网页的能力,还有一种使用场景,网页的开发者在使用HTML/JS/CSS开发网页的时候,发现能力不足,希望使用传统语言例如C/C++来开发一些库,这些库可以被网页调用,这样来满足应用的要求,这被称为混合编程,由此可看出NPAPI和PPAPI也能够提供混合编程的能力,也就是说,开发者能够将一些本地代码提供的能力提供给Web开发者,一下描述了使用PPAPI技术的混合编程:

<script>
  var exam = null;
  window.onload = function(){
    exam = document.getElementById("example");
    exam.addEventListener('message', handleMessage, false);
  }
  function handleMessage(message){
    ...// handle results here
  }
  function function1(){
    if (exam) exam.postMessage("function1");
  }
  function function2(){
    if (exam) exam.postMessage("function2");
  }
</script>
<embed id="example" type="application/x-example"/>
class SubInstance : public pp::Instance{
public:
  virtual bool HandleMessage(const pp::Var& var_message) {
    if (var_message == "function1") {
      ...
    } else if (var_message == "function2") {
      ...
    }
  }
};

从上面实例中可以看出它们的工作方式,两个JavaScript函数“function1”和“function2”可以被调用,这两个函数的实现通过c++代码来完成,因为PPAPI和NPAPI插件能够调用任何系统接口,所以开发者甚至能够将任何能力提供给JavaScript代码调用。从某种程度来说,使用插件机制来扩展JavaScript接口有个明显的缺陷,就是需要在DOM树中加入一个“embed”节点,而且需要提前完成插件的加载和初始化,但是从技术上来讲,开发者可以通过插件机制在JavaScript引擎中注入一些JavaScript对象和方法,使得这些JavaScript对象和方法能够调用本地代码才能提供的能力,这是一种不错的扩展JavaScript引擎方法。

JavaScript扩展机制

下面来看V8引擎和JavaScriptCore引擎是如何提供机制来扩展JavaScript引擎的能力,也就是如何使用本地代码来扩充引擎中的对象和函数,V8提供了两种方式,第一种是JavaScript绑定,第二种是V8的Extension机制,而JavaScriptCore提供了JavaScript绑定。

JavaScript绑定

WebKit中使用IDL来定义JavaScript接口(对象和方法),但是它又稍微不同于IDL的标准,对它做了一些改变,以适应WebKit的需要,如果开发者需要定义新的接口,那么需要完成以下一些步骤。首先需要定义新的接口文件下面的代码片段是一个简单的IDL文件:

module subModule {
  interface [
    InterfaceName=MyObject
  ] MyObj {
    readonly attribute long myattr;
    DOMString myMethod(DOMString myArg);
  };
}

它定义一个新的接口,该接口名为MyObj,它包含一个属性和一个函数,该接口属于模块”subModule“,根据定义如果开发者需要在JavaScript代码使用它们,方式是”subModule.MyObj.myAttr”和”subModule.MyObj.myMethod()”,看起来非常直观和容易理解。当开发者完成接口的定义之后,需要生成JavaScript引擎所需的绑定文件,该文件其实是按照引擎定义的标准接口为基础,来实现具体的接口类,WebKit提供了工具能够帮助开发者自动生成所需要的绑定类,根据引擎的不同和引擎开发语言的不同,可能有不同的结果,主要包括为JavaScriptCore和V8生成的绑定文件,以V8引擎为例,使用下面的命令就能偶为该IDL文件生成结果:

perl generate-bindings.pl MyObj.pl --generator=V8 --outputDir=./out

它会生成两个绑定文件V8MyObj.h和V8MyObj.cpp,这些绑定文件就是将JavaScript引擎的调用转成具体的实现类的调用,此外绑定文件需要调用开发者具体实现部分的代码,根据规则,本例需要包含开发者实现的MyObj.h 文件和MyObj类,下面的代码片段是所要实现的类,开发者只需要将两个函数实现即可被JavaScript引擎所调用:

class MyObj {
public:
  v8::Handle<v8::Value> myAttr(){...}
  v8::Handle<v8::Value> myMethod(const v8::Arguments& args){...}
};

JavaScript绑定机制的一个问题就是它需要和JavaScriptCore引擎或者是V8引擎一起编译,也就是说本地文件同引擎源代码一起编译生成本地代码,而不能在引擎只想能够之后动态的注入这些本地代码,这限制了它的使用场景,因为Web开发者需要能够在引擎中动态注入本地代码来提供一些新对象和函数以被JavaScript代码直接调用。

V8扩展

除了JavaScript绑定机制之外,V8引擎另外又提供一种能够动态扩展的机制,它无需跟V8引擎一起编译,因而有很大的灵活性。它的基本原理是提供一个基类“Extension”和一个全局的注册函数,对于想扩展JavaScript接口的开发者而言,只需要两个步骤既可以完成扩展JavaScript能力,下述代码描述了过程:

class MyExtension:public v8::Extension {
public:
  MyExtension():v8::Extension("v8/My", "native function my()"){}
  virtual v8::Handle<v8::FunctionTemplate> GetNativeFunction(v8::Handle<v8::String> name) {
    // 可以根据name来返回不同的函数
    return v8::FunctionTemplate::New(MYExtension::MY);
  }
  static v8::Handle<v8::Value> MY(const v8::Arguments& args) {
    // do something
    return v8::Undefined();
  }
};
MYExtension extension;
RegisterExtension(&extension);

第一个步骤是基于Extension基类构建一个它的子类,并实现它的重要虚函数,那就是GetNativeFunction,根据参数name来决定返回实现函数,第二个步骤是创建一个该子类的对象,并通过注册函数将该对象注册到V8引擎,这样当JavaScript调用“my”函数的时候就能够找到并执行响应的函数。从上面的代码可以看出,它只是调用V8的接口来注入新函数,所以这是一种动态扩展机制,非常的方面,但缺点是,理论上性能可能没有JavaScript绑定机制搞笑,因而只是在一些对性能要求不高的应用场景才会被使用到。

Chromium扩展机制

原理

Chromium的扩展机制原先是Chromium推出的一项技术,该机制能够扩展游览器的能力,从本质上来说,它就是游览器能力的简单扩展,而对于一些本地的功能,如书签、USB、蓝牙、电源管理等,该机制并没有这方面的能力。一个Chromium Extension的实例其实就是一个网页加上JavaScript代码和CSS样式代码,在Extension中,开发者也可以使用NPAPI插件和PPAPI及NaCl机制技术来扩展网页能力,因此它同这些技术没有冲突,相反,Chromium Extension机制可能需要这些技术来实现特定的功能。随着该机制的不断发展,Extension机制被用来支持Web应用程序,也就是使用HTML5、JavaScript、CSS等技术来开发应用程序,该应用程序可以使用Chromium游览器来运行,而用户获得的体验同本地应用程序非常接近。在目前的Chromium项目中,对于Web应用,Chromium根据特性将其分成两类,第一类叫Host App,另一种叫做Packaged App,前面一种表示将网络上的资源直接变成一个Web应用,所以它需要使用外部的资源才能够工作,而对于后一种,该Web应用所需要的文件和资源都包含在该应用中,而不需要外部的资源,因此对与那些离线应用特别有用,这让使用者感觉更像本地应用。因为目前的网页只是有HTML5、JavaScript和CSS等文件组成,所以还需要其他辅助功能才能形成一个Chromium的扩展实例,Chromium的Extension及时使用一个清单文件(manifest.json)来描述Extensions所需要的文件和资源等,这样使得它看起来更像一个应用程序,因为现在很多应用程序都使用该中方式,例如Android平台的AndroidManifest.xml,或者W3C为Web应用定义的Widget方式,下面实例了一个简单的清掉文件实例:

{
  "name":"Extension case",
  "description":"it is a extension example",
  "version":"1.0",
  "app":{
    "launch":{
      "web_url":"http://extension/example/"
    }
  },
  "icons":{
    "128":"128.png"
  },
  "permissions":[
    "notifications",
    "plugins",
    "management"
  ]
}

熟悉JavaScript语言的开发者会发现它其实是一个JSON格式的文件,里面的属性名也非常的直观,值得注意的是它包含了一个属性“permissions”,该属性设置了该Web应用能够访问哪些功能,例如“plugins”表明该Extension能够使用NPAPI插件,“notification”表示可以从该Extension发出通知,它同移动平台上的本地应用有类似的地方。其实,上面提供的这些权限所使用的功能,有些在HTML5中并没有被定义,但是Chromium的这些Extension实例能够使用它们,原因在于Chromium提供了一些JavaScript接口,就是著名的“chrome.*”应用程序编程接口,本质上它们是一系列的JavaScript接口,包括标签、管理、历史记录、USB等,功能还是非常的丰富,当Chromium的Extension实例需要使用这些接口的时候必须要在该清单文件中申明它们,否则Chromium会拒绝它们的请求,对于Chromium的Extension实例和Web应用,它们会共享一些接口,但是两者还会提供不同的接口,这是由于各自的目的不同,对于传统的Extension实例而言,这里面包含 “alarms”, “bookmarks”, “cookies”和”runtime”等,而对于Web应用而言,它们可以使用 “app.runtime”, “app.window”, “bluetooth”和 “runtime”等,这些接口也是对JavaScript能力的一种扩展,不同于NPAPI和PPAPI使用的扩展机制,”chrome.*“接口使用一种新的机制来处理多进程之间的通信,这依然是消息传递机制。

基本设施

针对Chromium的Extension机制,主要是解决两大方面的问题,第一是Extension实例的管理工作,包括安装、更新、删除等;第二是Extension实例是如何运行的。对于第一个问题,相关的过程比较复杂,这里主要介绍第二个问题,包括Extension运行时需要涉及到的基础设施,这同本章的重点JavaScript扩展密切相关,由于Extension运行时需要调用“chrome.*”接口,因此了解这些接口是如何被扩展和实现的。从基本过程上来看,是Chromium的Extension机制在V8引擎中注入了一些代码,然后当JavaScript代码调用这些接口的时候,V8引擎调用注入的本地代码,这些代码会将调用接口的请求从Render进程发送给Browser进程,在Browser进程中,接收这些请求并派发给相应的实现类,请求完成后按需要返回调用结果,对于Render进程,下图是Extension实例时所使用的一些主要类:

javascript扩展硬件设备 javascript 扩展插件_扩展_10

  • ChromeV8Context:对V8引擎上下文对象的一个简单封装,帮助注入代码和拥有Extension实例的本地实现函数列表
  • ModuleSystem:管理所有注册的“chrome.*”接口,这些接口的具体实现在Browser进程中,这里主要是注册回调的函数,这些回调函数会将接口的调用发送请求给Browser进程的具体实现类
  • NativeHandler,ObjectBackedNativeHandler,ChromeV8Extension:接口类(继承关系),用于表示每个“chrome.*”的接口,至于为什么有几层继承是因为需求,同时需要注册管理的回调函数
  • Dispatcher:该类负责同Render进程创建过程交互,也就是说它知道什么时候注入这些回调函数
  • EventBindings:实现chrome.events接口的辅助类,它会定义一个ChromeV8Extension的子类

下面是Browser进程的相关类

javascript扩展硬件设备 javascript 扩展插件_插件_11

  • ExtensionHost:负责处理请求的消息,并回复请求结果
  • ExtensionFunctionDispatcher:将请求转换成对ExtensionFunction的调用,因为如chrome.bookmarks这样的接口,包含多个函数,这里每个函数对应与一个ExtensionFunction对象
  • ExtensionFunction,AsyncExtensionFunction,BookmarksFunction和Bookmarks-GetFunction:用于表示接口中的函数,而BookmarksGetFunction对应的函数是”chrome.bookmarks.get()”

下面来看chromium是如何进行“chrome.*”接口初始化工作,主要是Render进程的工作。首先是网页的解释工作,在创建Document对象的时候,WebKit使用ScriptController类来注册Chromium的Extension机制所需要的代码,这里会调用类Dispatcher,该类此时创建一个ModuleSystem对象,并将各种各样的NativeHandler对象注册,这里的NativeHandler就是各种各样的”chrome.* “的接口,然后在注册这个NativeHandler的时候,每个该类的对象表示一个接口,每个类别的接口创建一个ObjectTemplate,该对象包含一个FunctionTemplate对象,当调用该接口的任何函数的时候就会调用ObjectBackedNativeHandler类的Router函数,最后在注册完之后完成网页渲染的工作,当执行到JavaScript代码调用”chrome.*”接口的时候,就会调用Router函数,之后就使用消息传递机制将请求传递给Browser进程。

消息传递机制

Chromium的扩展机制的一个重要的特定是使用消息传递机制来提供大量JavaScript新皆苦,前面提到V8引擎会调用Router方法,这里详细解释一个接口中的函数是如何使用消息传递机制来工作的。可以结合下面两张图来理解Extension机制中对“chrome.*”接口的函数调用过程,图中的数字表示调用顺序,首先是Render进程中的1过程,其次是Browser进程中的2过程,最后是Render进程中的3过程,其中两个进程都是通过消息传递机制来实现的,这里消息是将JavaScript函数中的所有调用转成字符串来处理,也就是当调用某个接口的时候,首先是在Render进程中,V8的引擎将使用接口名来查找NativeHandler,使用字符串来表示调用函数名,并将参数序列化成JSON格式的字符串传递给Browser进程,这些对函数的调用都是借助函数名和JSON字符串,称为Extension机制的消息传递机制:

javascript扩展硬件设备 javascript 扩展插件_插件_12


(V8引擎调用”chrome.*”接口在Render进程中的处理)

接着是下图2过程,经过一系列的处理之后,最终会调用具体的函数实现类,这里是BookmarkGetTreeFunction,读取JSON字符串并计算得出结果返回给Render进程,最后就是上图3的过程,得到回复之后,最后需要将它们传入V8引擎,这里是使用ChromeV8Context,它包含一个V8引擎运行上下文,使用该上下文将结果传入V8引擎:

javascript扩展硬件设备 javascript 扩展插件_html5_13


(Chromium在Browser进程中处理Extension的函数调用)