3 JavaScript引擎的扩展机制
3.1 混合编程
混合编程由来已久,因为浏览器能力的不足,特别是以前的浏览器甚至不支持内嵌视频和音频等技术,所以导致需要Flash等插件来扩展网页的能力。当然Flash插件是由第三方提供的,大家都可以使用。还有一种使用场景,那就是网页的开发者在使用HTML/JS/CSS开发网页的时候,发现能力不足,希望使用传统语言例如C/C++来开发一些库,这些库可以被网页调用,这样来满足应用的要求,这里称之为混合编程。
从上面的介绍可以看出,NPAPI和PPAPI也能够提供混合编程的能力,也就是说,开发者能够将一些本地代码提供的能力提供给Web开发者,示例代码10-4描述了使用PPAPI技术的混合编程。
示例代码10-4 使用插件机制来扩展JavaScript的功能
<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 MyInstance : 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引擎方式。
3.2 JavaScript扩展机制
下面看看V8引擎和JavaScriptCore引擎是如何提供机制来扩展JavaScript引擎的能力,也就是如何使用本地代码来扩充引擎中的对象和函数。V8提供了两种方式,第一种是JavaScript绑定,第二种是V8的Extension机制,而JavaScriptCore提供了JavaScript绑定。
3.2.1 JavaScript绑定
WebKit中使用IDL来定义JavaScript接口(对象和方法),但是它又稍微不同于IDL的标准,对它作了一些改变,以适应WebKit的需要。如果开发者需要定义新的接口,那么需要完成以下一些步骤。
首先,当然需要定义新的接口文件,示例代码10-5是一个简单的IDL文件,它定义一个新的接口,该接口名为MyObj,它包含一个属性和一个函数,该接口属于模块"mymodule"。根据这里的定义,如果开发者需要在JavaScript代码使用它们,方式是"mymodule.MyObj.myAttr"和"mymodule.MyObj.myMethod()",看起来非常直观和容易理解。
示例代码10-5 一个简单的IDL文件
module mymodule {
interface [
InterfaceName=MyObject
] MyObj {
readonly attribute long myAttr;
DOMString myMethod(DOMString myArg);
};
}
当开发者完成接口的定义之后,需要生成JavaScript引擎所需的绑定文件,该文件其实是按照引擎定义的标准接口为基础,来实现具体的接口类,WebKit提供了工具能够帮助开发者自动生成所需要的绑定类,根据引擎的不同和引擎开发语言的不同,可能有不同的结果,主要包括为JavaScriptCore和V8生成的绑定文件,以V8引擎为例,使用下面的命令就能够为该IDL文件生成结果。
perl generate-bindings.pl MyObj.pl --generator=V8 --outputDir=./out
它会生成两个绑定文件V8MyObj.h和V8MyObj.cpp,这些绑定文件就是将JavaScript引擎的调用转成具体的实现类的调用,示例代码10-6就是V8MyObj.cpp文件中的一个部分节选,主要是一个属性和方法的C++代码转接代码。
在V8MyObj.cpp中,还需要很多其他桥接的代码,它们都是辅助注册桥接的函数到V8引擎的。具体的做法是将示例代码10-6中的两个函数和它们的信息,放入一个下面的数组中。
{"myAttr", MyObjV8Internal::myAttrAttrGetter,0,0 /* no data */,static_cast<v8:: AccessControl>(v8::DEFAULT),static_cast<v8::PropertyAttribute> (v8::None), 0 /* on instance */}
之后将通过V8的注册函数V8DOMConfiguration::configureTemplate,将这些信息注册到引擎中去,有兴趣的读者可以自行根据上面的命令来生成该文件并理解这些辅助代码。
示例代码10-6 为V8引擎生成的绑定函数
static v8::Handle<v8::Value> myAttrAttrGetter(v8::Local<v8::String> name, const v8::AccessorInfo& info)
{
INC_STATS("DOM.MyObj.myAttr._get");
MyObj* imp = V8MyObj::toNative(info.Holder());
return v8Integer(imp->myAttr(), info.GetIsolate());
}
static v8::Handle<v8::Value> myMethodCallback(const v8::Arguments& args)
{
INC_STATS("DOM.MyObj.myMethod");
if (args.Length() < 1)
return throwNotEnoughArgumentsError(args.GetIsolate());
MyObj* imp = V8MyObj::toNative(args.Holder());
EXCEPTION_BLOCK(g*, myArg, V8g::HasInstance(MAYBE_MISSING_PARAMETER
(args, 0, DefaultIsUndefined)) ? V8g::toNative(v8::Handle<v8::Object>::
Cast(MAYBE_MISSING_PARAMETER(args, 0, DefaultIsUndefined))) : 0);
return v8String(imp->myMethod(myArg), args.GetIsolate());
}
绑定文件当然需要调用开发者具体实现部分的代码。根据规则,本例需要包含开发者实现的MyObj.h文件和MyObj类,示例代码10-7就是所要实现的类,开发者只需要将两个函数实现即可被JavaScript引擎所调用。
示例代码10-7 MyObj的实际实现类------MyObj.h
Class MyObj {
public:
v8::Handle<v8::Value> myAttr() {
…
}
v8:: Handle<v8::Value> myMethod(const v8::Arguments& args) {
…
}
};
JavaScript绑定机制的一个问题就是它需要和JavaScriptCore引擎或者是V8引擎一起编译,也就是说这些本地文件同引擎源代码一起编译生成本地代码,而不能在引擎执行之后动态地注入这些本地代码,这限制了它的使用场景,因为Web开发者需要能够在引擎中动态注入本地代码来提供一些新对象和函数,以被JavaScript代码直接调用。
3.2.2 V8扩展
除了JavaScript绑定机制之外,V8引擎另外又提供一种能够动态扩展的机制,它无须跟V8引擎一起编译,因而有很大的灵活性。
它的基本原理是提供一个基类"Extension"和一个全局的注册函数,对于想扩展JavaScript接口的开发者而言,只需要两个步骤即可以完成扩展JavaScript能力,如示例代码10-8所描述的过程。
示例代码10-8 使用V8的extension来注入新函数
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 sth here
return v8::Undefined();
}
};
MYExtension extension;
RegisterExtension(&extension);
第一个步骤是基于Extension基类构建一个它的子类,并实现它的重要虚函数,那就是GetNativeFunction,根据参数name来决定返回实现函数。第二个步骤就是创建一个该子类的对象,并通过注册函数将该对象注册到V8引擎,这样当JavaScript调用"my"函数的时候就能够找到并执行响应的函数。
从示例代码10-8可以看出,它只是调用V8的接口来注入新函数,所以这是一种动态扩展机制,非常的方便,但是缺点是,理论上性能可能没有JavaScript绑定机制高效,因而只是在一些对性能要求不高的应用场景才会被使用到。