2 Chromium PPAPI插件
2.1 原理
插件其实是一种统称,表示一些动态库,这些动态库根据定义的一些标准接口可以跟浏览器进行交互,至于这个标准接口是什么都可以,重要的是大家都遵循它们,NPAPI接口标准只是其中的一种,因为它被广泛使用,所以被提到的次数也最多。本节介绍的PPAPI也是一种浏览器和插件交互的接口标准,该标准是由Google提出,在Chromium项目中获得支持。
PPAPI的提出是因为NPAPI的可移植性和性能存在比较大的问题,特别是针对跨进程的插件,同时还有插件需要2D和3D绘图、声音等问题时候就更为棘手。早期的阶段就是要解决这些问题,同时为了赢得插件厂商的支持,尽可能地使用原来NPAPI的接口。现在,随着PPAPI的不断发展,接口不断发生改变。后来,PPAPI也被用在Native Client技术中,之后也被逐渐地修改,直到现在的样子,完整的列表可以查看链接http://code.google.com/p/ppapi/w/list。
那么,为什么PPAPI能够提供较高性能的绘图和声音等解决方案呢?前面我们提到,在现在的NPAPI插件系统中,通常的做法是,当网页需要显示该插件的时候或者需要更新的时候,它会发送一个失效(Invalidate)的通知,让插件来绘制它们。而在PPAPI插件机制中,它引入了一个保留(Retained)模式,其含义是浏览器始终保留一个后端存储空间,用来表示上一次绘制完的区域。这个很有用,因为PPAPI插件通常是跨进程的,所以浏览器可以绘制网页而不需要锁,与此同时插件进程能够在后台绘制新的结果。
PPAPI插件有两种运行模式,受信(Trusted)插件和非受信(Untrusted)插件。对于受信的PPAPI插件,它可以在Renderer进程中运行,也可以在另外的进程中运行。对于新版本的实现,架构设计都是基于IPC来设计的。对于非受信的PPAPI插件,则可以借助于使用NativeClient技术来安全运行。受信插件是与平台相关的,可以调用平台相关的接口。而对于非受信插件而言,它们可以是与平台无关的代码,可以调用NativeClient提供的有限接口,而不能调用其他接口,这个后面再介绍。
在Chromium中,NPAPI和PPAPI插件同时得到支持,都可以在“chrome://plugins”来查看,前面已经提到过。有趣的是,对于同一个功能的插件,甚至可能有两个不同的版本,如图10-8所示Flash的NPAPI插件和PPAPI插件实现。
图10-8 Chrome浏览器的NPAPI插件和PPAPI插件
PPAPI插件同样使用“embed”或者“object”元素,这让网页看起来没什么大的区别,所以对于WebKit而言,它根本不会区分背后的是NPAPI插件还是PPAPI插件,差别在于调用的接口不一样而已,这样做的好处显而易见。
2.2 结构和接口
2.2.1 代码结构
因为PPAPI插件对于WebKit而言是透明的,所以这里不再介绍WebKit中支持该插件的基础设施。Chromium支持跨进程的PPAPI插件机制,所以在代码结构上可以充分看到这一点。Chromium项目中有3个目录用来支持这一机制,详细结构如图10-9所示。
图10-9 支持PPAPI插件机制的代码目录结构
首先是chrome目录,它包含Renderer进程和Browser进程对于PPAPI插件的支持代码,主要是资源的实现类。
其次是content目录,同样也包含了资源的实现类,但是同时也有支持跨进程机制的代码,这会在后面的工作过程图中有所体现。
最后是ppapi目录,当然支持PPAPI插件的代码都是在该目录中,包括支持跨进程的基础代码,它们被Renderer进程和Browser进程的支持代码所使用。
读者可能好奇为什么chrome目录下的相关文件不直接放在content/目录下,这主要取决于Chromium项目的层次化结构。content目录下包含一些公共或者基础的设施,而chrome/目录则是跟浏览器密切相关的。例如对于PPAPI插件机制而言,它将PDF和Flash都放在该目录下,而将文件等放在content目录下。
2.2.2 应用程序编程接口
同NPAPI的NPN和NPP开头的接口相似,PPAPI也需要双向调用的编程接口,PPAPI提供了浏览器调用插件的接口,同时更是提供了众多插件调用浏览器各种功能的接口,这非常不一样,因为功能更为强大。
这些接口的标准定义文件都位于上面所述的目录ppapi/api中,它们都使用一种接口定义语言(IDL,Interface Definition Language)来描述。IDL是一种标准,有兴趣的读者可以查阅它的基本语法。其中以ppb_(ppapi browser)开头的接口文件表示这是由浏览器实现,被插件库所调用;以ppp_(ppapi plugin)开头的接口文件表示这是由插件实现,被浏览器所调用;而其他以pp_开头的接口文件表示共享的接口定义,两边都需要使用,主要是一些基础类定义等。
不同于NPAPI只是提供C接口,PPAPI既提供了C接口,同时又提供了C++接口。C接口主要是函数指针和结构为主,而C++接口则是提供各种作用的类,它们分别位于目录ppapi/c和ppapi/cpp下。因为两个定义的功能是一致的,之后我们都以C++接口为例来解释PPAPI插件机制。
公共部分的接口包括各个基础数据,如时间、大小、矩形和资源,这些类会作为后面定义接口的参数来传递,对应的接口例如PP_Time、PP_Size、PP_Rect和PP_Resource。这里面非常重要的接口是PP_Resource,它表示各种类型的资源,例如文件资源、音频资源、图像资源、图形资源等。
由插件实现的接口大致包括以下几个部分:第一部分是插件模块和插件实例,用于初始化和关闭插件的管理插件功能的接口,例如PPP_InitializeModule()、PPP_ShutdownModule()。而插件的实例类,表示一个插件的实例对象,也就是Interface PPP_Instance,这里面包含多个函数,如DidCreate、DidDestroy等,表示当创建插件之后,浏览器调用它们,以便插件能够做一些后续的辅助工作。第二部分是一些事件的通知接口,表示浏览器需要派发一些消息给插件,典型的包括鼠标事件、通用消息传递接口、3D图形上下文丢失事件和鼠标锁定事件等。
由浏览器实现的接口,主要提供各种能力给插件使用,这其中包括2D和3D图形绘制接口、文件IO、文件系统、鼠标事件、网络、游戏手柄、时间等,这些都是PPAPI机制中定义的可以被浏览器调用的资源及其编程接口,读者会发现这些主要都是为游戏的需求服务的,实际上这些机制就是为了高性能的游戏而设计的。
2.3 工作过程
2.3.1 基础设施
对于PPAPI插件的跨进程架构,同NPAPI插件的跨进程架构非常类似,可以说基本相同。同样当网页中出现一个“embed”元素的时候,PPAPI插件进程会为它创建一个插件实例,这里不再赘述。
对于插件模块和实例接口,由插件进程直接调用并根据需要加载和创建它们,非常的简单明了。在PPAPI插件机制中,复杂的是资源的调用,也就是浏览器提供给插件使用的各种资源接口,所以下面重点介绍围绕资源的基础设施。
图10-10描述了跨进程模式下PPAPI插件机制中资源是如何被插件调用的。如同前面一样,PPAPI的插件是在插件进程中被加载的,当它需要使用插件的时候,通过图中Thunk设施将C接口转成C++接口来调用相应的PluginResource类。该类是所有资源的基类,是一个代理类(只是将请求转发给真正的实现者),负责发送请求给其他进程,拥有接受其他进程发过来的调用结果的能力。发送请求由相应的其他类来帮助,在这里是PluginDispatcher类和HostDispatcher类。它们都会使用IPC::Channel来发送消息,消息会被Browser进程和Renderer进程中的BrowserPpapiHost类和RenderPpapiHost类处理(它们依赖ppapi/host的基类),这些请求会发送给ResourceHost类来处理,以调用真正的实现函数。读者发现这两个进程都有ResourceHost的子类,这是因为某些资源的实现在Renderer进程完成,例如2D和3D图形资源,但是有些类必须在Browser进程中处理,如文件和文件系统等。
图10-10 跨进程的PPAPI插件机制中支持资源的基础设施
2.3.2 工作过程
这里以Chromium中PPAPI的一个使用2D绘图的例子来说明PPAPI插件的工作过程,该例子位于目录ppapi/examples/2d下,主要有两个部分,一个是个简单的HTML网页文件(2d.html),另外一个就是实现插件的文件(paint_manager_example.cc)。示例代码10-1是使用插件的网页代码,示例代码主要是“embed”元素,同NPAPI插件的使用方式完全一模一样,下面会逐步讲解PPAPI插件的源代码,以及该插件是如何和该网页一起工作的。
示例代码10-1 使用插件的网页代码
<html> <head> <title>2D Example</title> </head> <body> <embed id="plugin" type="application/x-ppapi-example-2d"> </body> </html>
首先当然是插件的创建过程。在WebKit中,对于PPAPI和NPAPI的支持都是类似的,所以可以回顾10-7中的NPAPI插件被创建的过程,二者同样根据MIME类型来查找PPAPI插件机制,如果Chromium发现查找到的是一个PPAPI插件而不是NPAPI插件,那么在创建WebPluginContainerImpl对象的时候,就会首先创建一个WebPlugin子类的对象。注意,这里不是一个WebPluginImpl对象,而是一个PepperWebPluginImpl对象。之后它就发送消息到插件进程,请求创建一个插件的实例。回到插件进程,它会根据插件的注册信息查找需要的插件并调用它的构造函数来初始化该插件的模块,如示例代码10-2所示的CreateModule方法。之后需要调用该方法返回的对象来创建一个插件实例的对象,如示例代码中的CreateInstance方法,会创建一个插件类自定义的一个示例。
示例代码10-2 插件的实现代码部分节选
class MyModule : public pp::Module { public: virtual pp::Instance* CreateInstance(PP_Instance instance) { return new MyInstance(instance); } }; namespace pp { Module* CreateModule() { return new MyModule(); } }
根据示例代码10-2和示例代码10-3,当MyInstance被创建的时候,Chromium会创建PPP_Proxy_Instance对象,该对象接收从Renderer进程传递过来的关于该实例的状态消息,如插件视图改变、销毁等,然后再调用插件的相应接口,前面说过这些接口是在插件中实现并由浏览器调用的。
其次来了解资源的创建,一个插件实例可能会用到多个资源,如绘图资源、文件资源等,示例代码10-3所示的OnPaint函数使用到了2D绘图资源。由于它使用了PaintManager类,当需要更新视图的时候,该类需要创建一个Graphics2D资源对象。
示例代码10-3 插件的实例类自定义实现
class MyInstance : public pp::Instance, public pp::PaintManager::Client { public: MyInstance(PP_Instance instance) { … } virtual bool HandleInputEvent(…) { … } virtual void OnPaint(pp::Graphics2D& graphics_2d, …) { … } private: pp::PaintManager paint_manager_; };
为了详细说明它的调用过程,图10-11和图10-12描述了资源类对象的创建和资源类对象接口的调用过程,分别以插件进程和Renderer进程的交互为例,而插件进程和Browser进程的交互则是类似的情况。
图10-11 Chromium创建PPAPI插件的资源对象的过程
图10-11包括两个步骤,第一个步骤在插件进程中完成,第二个步骤在Renderer进程中完成。当PPAPI插件需要创建一个资源对象的时候,会通过PPAPI的C接口调用Chromium内部的实现,Thunk层将其转换成C++风格的调用。在插件进程中,会有一个工厂类来创建不同类型的资源对象。本例中Graphics2DResource对象在创建的同时会发送一个消息到Renderer进程,这就是第二步骤。Renderer进程同样包含一个能够创建不同类型ResourceHost对象的工厂类,以帮助完成资源对象的创建。
图10-12 PPAPI插件调用资源对象接口的过程
图10-12描述了当资源对象创建完之后,插件需要调用资源对象的接口来完成特定的操作,这一过程可以包含三个步骤,其发生在两个进程中,图中已有完整描述。首先,当然还是插件进程接收到插件的调用请求,并把请求发送给Renderer进程。其次是Renderer进程接收响应,然后执行特定的操作,并将结果值返回或者通知插件进程该动作执行完成。最后是插件进程接收到返回值或者动作执行完的消息,如果需要,它还可以调用插件的函数来通知插件。当然,某些调用不需要从Renderer进程返回结果到插件进程,所以前两步是必需的,但是第三步却是可选的。
2.4 Native Client
2.4.1 基本原理
NativeClient,也简称为NaCl,是一种沙箱技术,它能够提供给平台无关的不受信本地代码一个安全的运行环境,可以针对那些计算密集型的需求,例如游戏引擎、可视化计算、大数据分析、3D图形渲染等,这些场合只需要访问有限的一些本地接口,不需要通过网络服务来计算,以免占用额外的带宽资源。同时,它能够比较方便地将原来使用传统语言例如C++编写的库直接移植到Web平台中。它同WebGL、WebAudio这样的技术所解决的问题相似,但是途径不同,因为这些技术是规范(或者草案),而NativeClient技术是Google提出的。使用NativeClient能够将很多本地库的能力轻易地提供给网页使用,而不需要复杂的移植过程,给重用带来很大的方便。
本身PPAPI和NativeClient没有必然联系,两者解决的是不同方面的问题:PPAPI提供插件机制;NativeClient使用PPAPI的插件机制将使用NativeClient技术编译出来的本地库运行同浏览器交互起来。只是目前NativeClient是基于PPAPI接口来实现的,其实之前NativeClient也曾经基于NPAPI接口来实现,所以能够在Firefox、Safari和Opera浏览器中运行(目前显然不能了)。
因为NativeClient使用PPAPI来提供一个安全的运行环境,本身它也是一个PPAPI插件,图10-13就是Chrome浏览器中一个PPAPI插件——NativeClient。同其他的PPAPI插件不一样,它是一个在Renderer进程中运行的插件,因为这个插件显然是受信的,如图中的“Type: PPAPI(in-process)”。
图10-13 NativeClient的PPAPI插件
所以,如果需要在网页加入代码<embed id="plugin" type="application/x-nacl">表明使用了NaCl技术,对于WebKit而言,它就是调用一个插件(不知是什么类型的插件),对于PPAPI技术而言,它就是调用了内嵌的NaCl PPAPI插件。因为NaCl还需要调用开发者的本地库,所以它还需要跟本地库来交互,下面来介绍PPAPI插件之后的技术。
NaCl本质上是一个运行环境,该子系统提供了很少的一些受限系统调用接口和资源的抽象,本地库只能调用它们,而不能任意使用系统调用。与沙箱模型的不同在于,NaCl是将一个第三方开发的代码库运行在受限的环境中,而沙箱模型是将一个进程运行在受限环境中。NaCl提供编译工具,将使用C/C++代码编写的代码编译生成它能运行的可执行格式——nexe。本地代码调用的也都是本地接口,同JavaScript的交互都由NaCl机制和PPAPI机制来完成。
为了直观理解NaCl机制,用图10-14描述了它的架构图,该图参考Chromium官方网站的示意图,为进行了一些删减和修改。
图10-14 使用PPAPI技术的NaCl机制
首先研究一下Renderer进程,如果没有后面的部分,NaCl插件和其他PPAPI插件没有什么特别的差别,同样通过PPAPI跟渲染引擎进行通信。但是这里NaCl插件只是一个桥接工作,它将同浏览器(也可以说渲染引擎)的交互交接到使用NaCl技术的本地可执行库“nexe”。
然后,Chromium会创建一个新的进程,该进程使用了沙箱技术,只能访问特定的系统接口,这样限定了该进程中的任何库都不能超越它们。在Renderer进程中的NaCl插件使用消息传递机制同sel_ldr进程通信。在消息机制之上是一种称为SPRC(简单的进程远程调用技术),该机制可以实现PPAPI的跨进程调用。不过目前插件同NaCl的实现之间的通信机制SRPC已经不支持,都是通过一个新的接口PostMessage来实现的,该接口意味着都是通过消息机制来进行的。
最后,sel_ldr提供的环境能够运行nexe,从图10-14中可以看出,nexe是不受信的部分,但是没关系,该机制可以用一个沙箱来将它运行在限定的sel_ldr进程中,nexe没有办法使用NaCl提供的接口之外的系统接口,从而保证了安全。nexe是本地代码库,所以跟平台相关,例如对于32位和64位系统,需要两份不同的代码库,这同时也造成了库的冗余,有没有什么好的办法能够解决这个问题呢?在最近的版本中,Google的工程师开始将LLVM技术引入到NaCl机制中,这就是pNaCl技术。
2.4.2 pNaCI
nexe需要不同的版本,一个关键的问题在于NaCl提供的编译工具只能将NaCl的实现直接编译成同硬件架构相关的本地代码,所以不同的平台需要生成不同的本地库。pNaCl提供了一套新的工具,该工具能够将C/C++代码编译成LLVM字节码,读者回忆一下图9-27中关于LLVM的基本结构,LLVM能够将C/C++代码转成字节码,该字节码是平台无关的,而且该字节码可以保存起来,当字节码在某个平台上运行的时候,LLVM的后端能够根据字节码生成特定平台的本地代码。
回到pNaCl上来,使用了LLVM技术很明显地能够带来减少库的冗余性的好处,这样nexe就可以变成pexe(portable exe)。对于如何使用pNaCl,这里不再介绍,官方网站上有非常详细的介绍,感兴趣的读者请查阅下面的网页:http://www.chromium.org/nativeclient/pnacl/developing-pnacl。