(译)V8 JavaScript 引擎 – 嵌入者指南

译自:http://code.google.com/apis/v8/embed.html

如果你已经读过入门指导,你会已经熟悉把v8作为一个独立的虚拟机来使用,并且有了一些关于v8的概念,比如 handles, scopes, 和 contexts。这个文档将进一步的讨论这些概念,并介绍其他一些在嵌入v8到你的C++程序时需要了解的概念。

V8 的API提供了编译并执行脚本、访问 C++ 方法和数据结构、错误处理和安全检查。你的程序可以像使用其他库一样使用V8。你的C++代码可以在包含include/v8.h后通过使用 V8 API 来访问 V8。

V8 设计元素这个文档提供了一些你为 V8 而优化自己程序时有用的背景概念。

读者

本文档适用于想要将 V8 JavaScript 引擎嵌入到 C++ 程序中的 C++ 程序员。这将有助于你使用 JavaScript 调用你自己程序中的 C++ 对象和方法,并且可以让你的 C++ 程序调用 JavaScript 对象和函数。

句柄和垃圾收集

句柄提供了一个 JavaScript 对象在堆中位置的引用。V8 的垃圾收集器将把不再使用的对象的内存回收掉。在垃圾收集过程中,垃圾收集器经常吧对象移动到堆中的另外一个位置。当垃圾收集器移动一个对象时,垃圾收集器会把所有的指向该对象的句柄更新为对象的新地址。

如果一个对象无法从 JavaScript 中访问,并且没有句柄指向它时,它将被认定为垃圾。垃圾收集器不断的移除所有被认定为垃圾的对象。V8 的垃圾收集机制是V8性能的一个关键。可以通过查看V8设计元素来了解更多。

这里有两种类型的句柄:

*本地句柄。本地句柄在栈上,并且当指定的析构函数被调用时删除。这些句柄的生存时间由句柄范围决定,句柄范围经常在一个函数的开始的时候被创建。当句柄范 围被删除时,垃圾收集器将自由的析构掉那些前面在句柄范围内被句柄引用的对象,只要那些对象不再被JavaScript 访问或者不被其他的句柄引用。这类的句柄在入门的示例中使用过。

Local<SomeType>产生的本地句柄同样可以被存储在父类Handle<SomeType>声明的句柄里。

注意:句柄栈不是C++栈的一部分,但是句柄范围是嵌入到C++栈中的。句柄范围只能是在栈上分配的,不能通过new来分配。

* 持久句柄。持久句柄并不在栈上分配,并且只有你指定移除它们时才被删除。就想一个本地句柄一样,一个持久句柄将提供一个堆上分配对象的引用。当你需要在不止一个函数中使用一个对象的引用或者句柄生命并不对应于 C++ 的范围时,你可以使用一个持久句柄来保持对那个对象的引用。例如, Google Chrome 使用持久句柄来引用 DOM 节点。一个持久的句柄通过 Persistent::New 来创建,并且通过Persistent::Dispose 来销毁。持久句柄可以通过 Persistent::MakeWeak 来设置为弱,当对一个对象的引用都是弱的持久句柄时将触发垃圾收集器的一个回调函数。

Persistent<SomeType> 产生的持久句柄同样可以被存储在父类 Handle<SomeType> 声明的句柄里。

当然,每次你创建一个对象的时候都创建一个句柄将会产生很多句柄。这就是为什么句柄范围非常有用。你可以把句柄范围看做一个持有许多句柄的容器。当句柄范围的析构函数被调用时,所有在那个范围中创建的句柄都将被从栈中移除。如你所愿,这将导致句柄指向的对象被垃圾收集器从堆上删除。

回到我们在入门中非常简单的例子上,在下面的的图表中,你可以看到基于栈和基于堆的对象。注意 Context::New() 返回了一个不在句柄栈上的持久句柄。

当析构函数 HandleScope::~HandleScope 被调用时,句柄范围将被删除。在这个被删除句柄范围中句柄引用的对象如果没有其他的引用的话,将在下一次的垃圾收集中被移除。当 source_objscript_obj 不再继续被其他的句柄引用或者不能再从 JavaScript 中访问的时候,垃圾收集器也会从堆上把他移除。因为 context 句柄是一个持久句柄,他在句柄范围退出时并不会被删除。只能通过显式的调用 Dispose 来删掉context句柄。

注意:在这个文档中简写的句柄指本地句柄,当讨论到一个持久句柄时将使用全称。

Context

在V8中,context 是一个允许独立无关的 JavaScript 应用运行在一个独立的V8实例的执行环境。你必须显式的指定你要运行的JavaScript 代码的context 。

为什么是必须的?因为 JavaScript 提供了一系列 JavaScript 代码可以修改的内置方法和对象。例如,如果两个无关的JavaScript 对象都用同一个方法修改了全局对象,那么可能会发生未知的结果。

从CPU时间和资源上考虑,为若干必须内置的对象而创建一个新的执行环境代价比较昂贵。但是,V8 的扩展缓存机制保证你在创建第一个context 可能比较昂贵,但是子 context 会比较廉价。这是因为第一个 context 需要去创建内置对象并且解析内置的 JavaScript 代码,而子 context 只需要为它的 context 创建内置对象。v8的快照功能(默认打开,编译选项snapshot=yes来激活)花时间创建的第一个context将是非常优化的一个快照,因为它包含了一个已经为内置 JavaScript 代码编译好的代码的序列化的堆。同垃圾收集一样, V8 的扩展缓存机制也是v8性能的一个关键,更多信息可以参考  V8 设计元素

当你创建了一个 context 后,你可以进入和退出任意次。当你在context A时你也可以进入另外一个context B,这意味着你用B替换A来作为你的当前context。当你退出B后,A就被还原为当前环境。图解如下:

注意:内置的方法和对象对每个context来说是独立的。你可以有选择的为你的context设置安全令牌。更多的信息请查看安全模块部分。

在 V8 context的驱动下,浏览器的每个窗口和框架都可以有他自己干净的 JavaScript 环境。

模板

模板是 JavaScript 函数和对象在一个context中的蓝图。你可以用模板去把 C++ 函数和数据结构包装到 JavaScript 对象中,这样他们就可以被JavaScript 脚本调用了。例如,Google Chrome 就使用模板把 C++ DOM 节点包装成一个 JavaScript 对象,并且在全局命名空间中设置了一些函数。你可以创建一系列的模板,并在你创建的每个新 context 中使用同样的他们。你可以创建你需要的任意多的模板。但是在任何给定的 context中,对任意模板只能有一个实例。

在 JavaScript 中,函数和对象有很强的二元性。在 Java 和 C++ 中你可以通过显示的定义一个类来创建一种新的对象。在 JavaScript 中,你可以创建一个新的函数,并且使用函数作为构造函数来创建一个实例。一个JavaScript 对象的构造和功能是同构造他的函数紧密相连的。这反映在了V8 模板的工作方式中。有两种模板:

*函数模板
函数模板是一个独立函数的蓝图。在你希望初始化 JavaScript 函数的 context 中,你可以通过调用模板的GetFunction方法来建立一个模板的JavaScript 实例。你也可以将一个 C++ 回调函数同一个函数模板关联起来,当 JavaScript 函数实例被调用的时候它将被调用。

*对象模板
每个函数模板都有一个关联的对象模板。这是用来配置通过用这个函数作为构造函数创建的对象。你可以关联两种 C++ 回调函数到对象模板上:
– 访问器回调函数,当指定的对象属性被脚本访问的时候被调用。
– 拦截器回调函数,当任意对象属性被脚本访问的时候被调用。
访问器和拦截器将在本文档的后面讨论。

下面的代码提供了一个为全局对象创建模板并且设置全局函数的例子。

// Create a template for the global object and set the
// built-in global functions.
Handle<ObjectTemplate> global = ObjectTemplate::New();
global->Set(String::New("log"), FunctionTemplate::New(LogCallback));
 
// Each processor gets its own context so different processors
// do not affect each other.
Persistent<Context> context = Context::New(NULL, global);

这个示例代码摘自 process.cc 实例中的 JsHttpProcessor::Initializer

访问器

访问器是一个计算并且返回一个值的 C++ 回调函数,将在一个对象的属性被 JavaScript 脚本访问时调用。访问器通过对象模板的SetAccessor方法设置。这个方法需要被关联属性的名字和在脚本试图读写该属性时调用的两个回调函数。

访问器的复杂度取决于你操纵的数据类型。

访问静态全局变量

比方说,这里有两个C++整形变量 xy 需要在一个 context 中被 JavaScript 作为一个全局变量来访问。 要做到这一点,每当一个脚本需要去读写这些变量时,你需要去调用 C++ 访问器函数。这些访问器函数通过使用 Integer::New 转换一个 C++ 整形到一个 JavaScript 整形,并且使用Int32Value 去转换一个 JavaScript 整形到一个 C++ 整形。示例代码如下:

  Handle<Value> XGetter(Local<String> property, 
                        const AccessorInfo& info) {
    return Integer::New(x);
  }
 
  void XSetter(Local<String> property, Local<Value> value,
               const AccessorInfo& info) {
    x = value->Int32Value();
  }
 
  // YGetter/YSetter are so similar they are omitted for brevity
 
  Handle<ObjectTemplate> global_templ = ObjectTemplate::New();
  global_templ->SetAccessor(String::New("x"), XGetter, XSetter);
  global_templ->SetAccessor(String::New("y"), YGetter, YSetter);
  Persistent<Context> context = Context::New(NULL, global_templ);

注意:代码中的对象模板在 context 创建的时候就被创建。模板可以被创建并在任意数量的 context 中使用。

访问动态变量

在先前的示例中,变量是静态的,全局的。如果被操纵的数据是动态的呢?比如浏览器中实际使用的 DOM 树。让我们把 x y看作是C++类Point 的成员吧:

  class Point {
   public:
    Point(int x, int y) : x_(x), y_(y) { }
    int x_, y_;
  }

为了使任意数量的C++ point实例可以在 JavaScript 中使用,我们需要去对每一个C++ point创建一个JavaScript 对象,并且在JavaScript 对象和 C++ 实例中建立关联。这可以通过外部值和内部对象字段来实现。

首先需要为point包装对象创建一个对象模板:

  Handle<ObjectTemplate> point_templ = ObjectTemplate::New()
;

每个JavaScript point对象都保持一个C++对象的关联,因为他包裹了一个内部字段。这些字段被这样命名,因为他们不能从JavaScript 中访问,他们只能从C++代码中访问。一个对象可以有任意数量的内部字段,内部字段的数量通过下面的代码来设定。

  point_templ-&gt;SetInternalFieldCount(1);

这里内部字段的数量被设置为1,这意味着对象有一个索引为0的内部字段,这个字段指向一个C++对象。

在模版中添加xy的访问器:

  point_templ.SetAccessor(String::New("x"), GetPointX, SetPointX);
  point_templ.SetAccessor(String::New("y"), GetPointY, SetPointY);

下一步,通过创建一个模版的实例来包装一个C++ point,并且设置point p外部包装的内部字段为0.

  Point* p = ...;
  Local<Object> obj = point_templ->NewInstance();
  obj->SetInternalField(0, External::New(p));

这个外部对象只是简单的包装了一个 void* 。外部对象只能用于在内部字段中存放一个引用。JavaScript 对象不能直接指向 C++对象,所以这个外部值是作为从 JavaScript 到 C++ 的桥梁来使用的。从这方面上来讲,外部对象与句柄是相对应的,因为句柄让 C++ 可以引用到 JavaScript 对象。

这就是 xget set 的访问器的定义,y 的访问器定义与 x 相同,只须把 x 替换为 y 就可。

  Handle<Value> GetPointX(Local<String> property,
                          const AccessorInfo &info) {
    Local<Object> self = info.Holder();
    Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
    void* ptr = wrap->Value();
    int value = static_cast<Point*>(ptr)->x_;
    return Integer::New(value);
  }
 
  void SetPointX(Local<String> property, Local<Value> value,
                 const AccessorInfo& info) {
    Local<Object> self = info.Holder();
    Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
    void* ptr = wrap->Value();
    static_cast<Point*>(ptr)->x_ = value->Int32Value();
  }

访问器释放了本来被 JavaScript 对象包装起来的 point 对象的引用,并且读写关联的字段。这样的话,这些通用的访问器可以用在任意数量的包装的 point 对象上。

拦截器

你同样可以指定一个回调函数给一个脚本访问任意对象的属性时被调用。这叫做拦截器。为了效率,这里有两种拦截器。

* 名称属性拦截器 – 当通过字符串名称访问属性时被调用。例如,在浏览器的环境下,document.theFormName.elementName

* 索引属性拦截器 – 当通过索引访问属性时被调用。例如,在浏览器的环境下,document.forms.elements[0]

随 V8 源码提供的示例 process.cc 中包括一个使用拦截器的示例。在下面的代码片段中 SetNamedPropertyHandler 指定了 MapGet MapSet 拦截器。

Handle<ObjectTemplate> result = ObjectTemplate::New();
result->SetNamedPropertyHandler(MapGet, MapSet);

MapGet 拦截器如下所示:

Handle JsHttpRequestProcessor::MapGet(Local name,
                                             const AccessorInfo &amp;info) {
  // Fetch the map wrapped by this object.
  map *obj = UnwrapMap(info.Holder());
 
  // Convert the JavaScript string to a std::string.
  string key = ObjectToString(name);
 
  // Look up the value if it exists using the standard STL idiom.
  map::iterator iter = obj-&gt;find(key);
 
  // If the key is not present return an empty handle as signal.
  if (iter == obj-&gt;end()) return Handle();
 
  // Otherwise fetch the value and wrap it in a JavaScript string.
  const string &amp;value = (*iter).second;
  return String::New(value.c_str(), value.length());
}

像访问器一样,当一个属性被访问时指定的回调函数就会被激活。访问器和拦截器的区别就在于拦截器处理所有的属性,而访问器只与指定的属性相关联。

安全模型

同源策略(首先在Netscape Navigator 2.0引入)阻止从一个来源加载的脚本和文档去访问或设置另外一个来源的文档。同源在这里是指域名 (www.example.com),、协议 (http 或 https)和端口 (例如, www.example.com:81 与 www.example.com 是不同的)相同。这三个都相同的两个网页才能被认定为同源。没有这个保护的话,恶意的网页可能危害到另外一个正常的网页。

在 V8 中,同源被定义为一个 context。访问任何一个并非调用你的 context 在默认情况下是被阻止的。为了访问并非调用你的context,你需要使用安全令牌,或者安全回调函数。一个安全令牌可以是任何值,但通常是一个不在其他地方存在的符号或者规范的字符串。在你建立一个context时,你可以有选择使用SetSecurityToken指定一个安全令牌。如果你不指定安全令牌的话,V8 会自动为你创建的 context 生成一个。

当试图访问一个全局变量时,V8 的安全系统会首先检查被访问的全局对象的安全令牌并同试图访问全局对象的代码的安全令牌做对比。如果令牌匹配的话,访问就被允许。如果令牌不匹配的话, V8 会调用回调函数来判断这个访问是否应该被允许。你可以通过使用对象模版的 SetAccessCheckCallbacks 方法设置对象的安全回调函数,以此来指定是否允许访问该对象。V8的安全系统可以获取被访问对象的安全回调函数,并且调用它来判断是否允许另外一个 context 去访问它。被访问的对象、被访问的属性名、访问的类型(例如读、写和删除)将被传递给回调函数,回调函数会返回是否允许访问。

Google Chrome 中实施了这个机制,所以一旦安全令牌匹配不上,指定的回调函数会只允许以下操作:window.focus(), window.blur(), window.close(), window.location, window.open(), history.forward(), history.back(),history.go().g

异常

当错误发生时,v8会抛出一个异常。比如,当一个脚本或者函数试图去访问一个并不存在的属性,或者调用一个并不是函数的函数。

当操作失败的时候V8会返回一个空句柄。所以在继续执行前,在代码中检查返回值是否为一个空句柄是非常重要的。可以通过 Handle 类中的公共函数 IsEmpty() 来判断一个句柄是否为空。

你可以使用TryCatch来捕获异常,如下所示:

  TryCatch trycatch;
  Handle v = script-&gt;Run();
  if (v.IsEmpty()) {
    Handle exception = trycatch.Exception();
    String::AsciiValue exception_str(exception);
    printf("Exception: %s\n", *exception_str);
    // ...
  }

如果返回值是一个空句柄,并且你没有正确的设置TryCatch,你的代码就会崩溃。如果你设置一个TryCatch,异常就会被捕获,你的代码可以继续执行。

继承

JavaScript 是一个类型无关,面向对象的语言,并且其本身使用的是原型继承而非类继承。这可能会使受传统面向对象语言(比如C++ 和 Java.)培训的程序员感到疑惑。

基于类的面向对象的语言,比如Java 和 C++,是建立在两种截然不同的实体概念上的:类和实例。JavaScript 是一个原形继承的语言,并且不会有这种区别:它只有对象。JavaScript 不支持原生的类型继承声明;但是,JavaScript 的原型机制简化了添加自定义属性和方法到一个对象所有实例的过程。在 JavaScript 中,你可以添加自定义的属性到对象上。例如:

// Create an object "bicycle"
function bicycle(){
}
// Create an instance of bicycle called roadbike
var roadbike = new bicycle()
// Define a custom property, wheels, on roadbike
roadbike.wheels = 2

这种方式添加的自定属性值只存在于对象的这个实例上。如果我们创建了另外一个叫做 mountainbike bicycle() 实例,mountainbike.wheels会返回未定义,除非我们显式的添加 wheels 属性。

有时候这就是我们所需要的,但是在另外一些情况下,添加自定义属性到一个对象的所有实例可能才是我们需要的,毕竟所有的 bicycles 都有 wheels。这就是为什么原型继承的 JavaScript 非常有用。为了使用原型对象,在添加自定义属性之前,需要用到对象的 prototype

// First, create the "bicycle" object
function bicycle(){
}
// Assign the wheels property to the object's prototype
bicycle.prototype.wheels = 2

所有 bicycle() 的实例都会有预置的 wheels 属性。

同样的方法也在 V8 的模版中使用了。每个 FunctionTemplate 都有一个 PrototypeTemplate 方法,这个方法给出了函数原型的模版。你可以设置原型并且关联 C++ 方法到那些原型上,通过 PrototypeTemplate 那些方法和属性会出现在所有对应的 FunctionTemplate 上。比如:

 Handle biketemplate = FunctionTemplate::New();
 biketemplate.PrototypeTemplate().Set(
     String::New("wheels"),
     FunctionTemplate::New(MyWheelsMethodCallback)
 )

这会导致所有 biketemplate 的实例在他们的原型链中都有一个 wheels 方法,当 wheels 被调用的时候会激活 C++ 函数 MyWheelsMethodCallback

V8的 FunctionTemplate 类提供公共成员函数 Inherit() ,你可以在你想要一个函数模版去继承另外一个函数模版时调用,如下:

void Inherit(Handle parent);

5 Replies to “(译)V8 JavaScript 引擎 – 嵌入者指南”

  1. Pingback: about v8 | SunChrome

  2. 这个翻译不是很准确:“模板是 JavaScript 函数和对象在一个context中的蓝图。”

    我翻译如下,不过也不是很准确
    “template 是Context中为JS函数和对象的一种设计。”

    • to sc:呵呵,菜鸟一只,翻译的也比较烂。自己看书的时候最讨厌那些翻译的烂的了,没想到自己竟然做这种事。

  3. Pingback: 人自明 » About V8 js

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

*