[译] V8 使用者文档

若是你已经阅读过了上手指南,那么你已经知道了如何做为一个单独的虚拟机使用 V8 ,而且熟悉了一些 V8 中的关键概念,如句柄上下文。在本文档中,还将继续深刻讨论这些概念而且介绍其余一些在你的 C++ 应用中使用 V8 的关键点。c++

V8 的 API 提供了编译和执行脚本,访问 C++ 方法和数据结构,处理错误和启用安全检查的函数。你的应用能够像使用其余的 C++ 库同样使用 V8 。你的 C++ 应用能够经过引入头文件 include/v8.h 来访问 V8 API 。程序员

当你想要优化你的应用时,V8 设计概要文档能够提供不少有用的背景知识。编程

前言

这篇文档的受众是那些想要在本身的 C++ 程序中使用 V8 JavaScript 引擎的人。它将能帮助你在 JavaScript 中使用你应用中的 C++ 对象和方法,亦能帮助你在 C++ 应用中使用 JavaScript 对象和方法。数组

句柄和垃圾回收

一个句柄提供了对在堆中的一个 JavaScript 对象地址的引用。V8 的垃圾回收器会在该对象不能再次被访问到时,将其回收。在垃圾回收的过程当中,垃圾回收器可能会改变对象在堆中的位置。当垃圾回收器移动对象时,全部引用到该对象的句柄也会被一同更新。浏览器

当一个对象在 JavaScript 中已经不可被访问而且没有任何指向它的句柄时,它就会被垃圾回收。V8 的垃圾回收机制是 V8 性能表现的关键。更多信息可参阅 V8 设计概要文档缓存

句柄分为许多种:安全

  • 本地句柄会被分配在栈中,而且当对应的析构函数被调用时,它也会被删除。这些本地句柄的生命周期取决于它对应的句柄域(handle scope),句柄域一般在一个函数调用的开始被建立。当句柄域被删除时,垃圾回收器就能够清除以前分配在该句柄域中的全部句柄了,由于它们不能再被 JavaScript 或其余句柄所访问到。这也正是在上手指南中使用的句柄。
    本地句柄可经过类 Local<SomeType> 建立。数据结构

注意:句柄栈并非 C++ 调用栈中的一部分,但句柄域却在 C++ 栈中。故句柄域不可经过 new 关键字来分配。app

  • 持久句柄提供了对分配在堆中的 JavaScript 对象的引用。当你须要在超过一次函数中保持对一个对象的引用时,或者句柄的生命周期与 C++ 的块级域不相符时,你应使用持久句柄。例如,在 Google Chrome 中,持久句柄被用来引用 DOM 节点。一个持久句柄能够经过 PersistentBase::SetWeak ,变为弱(weak)持久句柄,当一个对象所剩的惟一引用是来自于一个弱持久句柄时,便会触发垃圾回收。编程语言

    • 一个 UniquePersistent<SomeType> 依赖于 C++ 构造函数和析构函数来管理下层对象的生命周期。

    • 一个 Persistent<SomeType> 能够经过自身的构造函数来建立,但必须手动地经过 Persistent::Reset 来清除。

  • 还有两种不多会被使用到的句柄,在这里咱们仅对它们作一个简单的介绍:

    • 永久(Eternal)句柄是一种为被认为永远不会被删除的 JavaScript 对象所设计的持久句柄。它的建立开销更低,由于它不须要被垃圾回收。

    • Persistent<SomeType>UniquePersistent<SomeType> 都不能被复制,因此它们不能做为 C++ 11 以前的标准库容器中的值来使用。PersistentValueMapPersistentValueVector 为它们提供了容器类,提供了相似于集合和向量的语义。

固然,每当你为了建立一个对象而建立一个本地句柄时,每每可能会致使建立了许多句柄。这就是句柄域所存在的意义。你能够将句柄域视做一个保存了许多句柄的容器。当句柄域的析构函数被调用时,全部它里面的句柄都会被从栈中移除。正如你所指望的,这些句柄作指向的对象随后就能够被垃圾回收。

回到咱们在上手指南中的例子,在下面的图示中,你能够看到句柄栈和堆中的对象。值得注意的是,Context::New() 的返回值是一个本地句柄,而后咱们基于它建立了一个新的持久句柄,用以阐述持久句柄的用途。

local_persist_handles_review.png

HandleScope::~HandleScope 析构函数被调用时,该句柄域便会被删除。全部被句柄域中的句柄所引用的对象,都将能够在下次垃圾回收时被删除,若是没有其余对于它们的引用存在。垃圾回收器一样还会删除堆中的 source_objscript_obj 对象,由于它们不被任何句柄所引用,而且也不可被 JavaScript 访问。因为 context 对象是一个持久句柄,因此当句柄域退出时,它并不会被移除,惟一能够删除它的办法就是调用它的 Reset 方法。

注意:后文中的句柄若是不加注明,都指的是本地句柄。

在这个模型下,有一个很是常见的陷阱须要注意:你不能够直接地在一个声明了句柄域的函数中返回一个本地句柄。若是你这么作了,那么你试图返回的本地句柄,将会在函数返回以前,在句柄域的析构函数中被删除。正确的作法是使用 EscapableHandleScope 来代替 HandleScope 建立句柄域,而后调用 Escape 方法,而且传入你想要返回的句柄。例子:

// 这个函数会返回一个带有 x,y 和 z 三个元素的新数组
Local<Array> NewPointArray(int x, int y, int z) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();

  // 咱们将会建立一些临时的句柄,因此咱们先建立一个句柄域
  EscapableHandleScope handle_scope(isolate);

  // 建立一个空数组
  Local<Array> array = Array::New(isolate, 3);

  // 若是在建立数组时产生异常,则返回一个空数组
  if (array.IsEmpty())
    return Local<Array>();

  // 填充数组
  array->Set(0, Integer::New(isolate, x));
  array->Set(1, Integer::New(isolate, y));
  array->Set(2, Integer::New(isolate, z));

  // 经过 Escape 返回该数组
  return handle_scope.Escape(array);
}

Escape 方法复制参数中的值至一个封闭的域中,而后删除其余本地句柄,最后返回这个能够被安全返回的新句柄副本。

上下文

在 V8 中,上下文是一个容许多个分别独立的,不相关的 JavaScript 应用在一个单独的 V8 实例中运行的执行环境。你必须为每个你想要执行的 JavaScript 代码指定一个上下文。

这样作是必要的么?这么作的缘由是,JavaScript 自己提供了一组内建的工具函数和对象,但它们又能够被 JavaScript 代码所修改。例如,两个彻底没有关联的 JavaScript 函数同时修改了一个全局对象,那么可能就会形成不可预期的后果。

从 CPU 时间和内存的角度来看,建立一个拥有指定数量的内建对象的执行上下文彷佛开销很大。可是,V8 的缓存机制能够确保,虽然建立的第一个上下文开销很是大,但后续的上下文建立的开销都会小不少。这是由于第一次建立上下文时,须要建立内建对象和解析内建的 JavaScript 代码,然后续的上下文建立则只需为它们的上下文建立内建对象便可。若是开启了 V8 的快照特性(可经过选项 snapshot=yes 开启,默认值即为开启),第一次建立上下文的时间花销也会被极大的优化,由于快照中包含了这些所需的内建 JavaScript 代码已然被编译后的版本。除了垃圾回收外,V8 的缓存也是 V8 性能表现的关键,更多详情可参阅V8 设计概要文档

当你建立了一个上下文后,你能够随意地进入和离开它,没有次数的限制。当你已经在上下文 A 中时,你还能够再次进入另外一个上下文 B ,这觉得着当前的上下文环境变成了 B 。当你离开了 B 后,A 就再次成为了当前上下文。如图示:

intro_contexts.png

须要注意的是,JavaScript 内建工具函数和对象是相互独立的。当你建立一个上下文时,你能够同时设置可选的安全标识(security token)。更多详情请参阅下文的安全模型章节。

在 V8 中使用上下文的最初动机是,在一个浏览器中,每个窗口和 iframe 都须要有各自独立的 JavaScript 环境。

模板

一个模板即为一个上下文中 JavaScript 函数和对象的蓝图。你能够在 JavaScript 对象内使用一个模板来包裹 C++ 函数和数据结构,导致它们能够被 JavaScript 脚本所操纵。例如,Google Chrome 使用模板来将 C++ DOM 节点包裹为 JavaScript 对象,而后在全局命名空间下注册函数。你能够建立一个模板集合,而后在不一样的上下文中使用它。模板的数量并无限制,可是在一个指定的上下文中,每个模板都只容许有一个它的实例。

在 JavaScript 中,函数和对象间有强烈的二元性。在 Java 或 C++ 中,若是要建立一个新类型的对象,你须要首先定义一个新的类。而在 JavaScript 中,你须要定义一个新的函数,而后把这个函数视做一个构造函数。一个 JavaScript 对象的外形和功能都与它的构造函数关系密切。这些也都反应在了 V8 模板的工做方式中。模板分为两种类型:

  • 函数模板
    一个函数模板就是一个独立函数的蓝图。在一个你想要实例化 JavaScript 函数的上下文中,你能够经过调用模板的 GetFunction 方法来建立一个模板的 JavaScript 实例。当 JavaScript 函数实例被调用时,你还能够为模板关联一个 C++ 回调函数一同被调用。

  • 对象模板
    每个函数模板都有一个与之关联的对象模板。对象模板用来配置将这个函数做为构造函数而建立的对象。你能够为对象模板关联两种类型的 C++ 回调:

    • 访问器回调会在指定对象原型被脚本访问时被调用。

    • 拦截器回调会在任何对象原型被脚本访问时被调用。

访问器和拦截器的详情会在后文中继续讨论。

下面的例子中,咱们将建立一个关联全局对象的模板,而后设置一些内建的全局函数。

// 建立一个关联全局对象的模板,而后设置一些内建的全局函数。
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(String::NewFromUtf8(isolate, "log"), FunctionTemplate::New(isolate, LogCallback));

Persistent<Context> context = Context::New(isolate, NULL, global);

该例子取自于 process.cc 中的 JsHttpProcessor::Initialiser

访问器

访问器为当一个 JavaScript 对象原型被脚本访问时,执行的一个 C++ 回调函数,它计算并返回一个值。访问器须要经过一个对象模板来配置,经过它的 SetAccessor 方法。这个方法的第一个参数为关联的属性,最后一个参数为当脚本试图读写这个属性时执行的回调。

访问的复杂度取决于你想要其控制的数据类型:

  • 访问静态全局变量

  • 访问动态变量

访问静态全局变量

假设有两个名为 xy 的 C++ 整形变量,它们须要成为一个上下文的 JavaScript 中的全局变量。为了达成这个目的,当脚本读或写这些变量时,你须要调用 C++ 访问器函数。这些访问器函数使用 Integer::New 来把 C++ 整形数转换为 JavaScript 整形数,而且使用 Int32Value 来把 JavaScript 整形数转换为 C++ 整形数。例子:

void XGetter(Local<String> property,
                const PropertyCallbackInfo<Value>& info) {
    info.GetReturnValue().Set(x);
  }

  void XSetter(Local<String> property, Local<Value> value,
               const PropertyCallbackInfo<Value>& info) {
    x = value->Int32Value();
  }

  // YGetter/YSetter 十分相似,这里就省略了

  Local<ObjectTemplate> global_templ = ObjectTemplate::New(isolate);
  global_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), XGetter, XSetter);
  global_templ->SetAccessor(String::NewFromUtf8(isolate, "y"), YGetter, YSetter);
  Persistent<Context> context = Context::New(isolate, NULL, global_templ);

注意代码中的对象模板和上下文几乎在同时建立。模板能够提早建立好,而后在任意数量的上下文中使用它。

访问动态变量

在上面的例子中,变量是静态和全局的。那么,若是数据是动态的,像浏览器中的 DOM 树这样呢?假设咱们有一个 C++ 类 Point,它有两个属性 xy

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

为了让任意数量的 C++ point 实例能够经过 JavaScript 访问,咱们须要为每个 C++ point 实例建立一个 JavaScript 对象。这能够经过外部(external)值和内部(internal)属性共同办到。

首先建立一个对象模板,用以包裹 point 实例:

Local<ObjectTemplate> point_templ = ObjectTemplate::New(isolate);

每个 JavaScript 中的 point 对象都保持了对 C++ 对象的引用,由于它之内部属性的方式被包裹。这些属性不可经过 JavaScript 访问,只能经过 C++ 代码访问到。一个对象能够有任意数量的内部属性,这个数量需经过如下方法来设置:

point_templ->SetInternalFieldCount(1);

上面的例子中,内部属性的数量被设置为了 1,代表这对象有一个内部属性,索引值为 0。

向模板添加 xy 访问器:

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

接下来,咱们经过建立一个新的模板实例来包裹 C++ point 实例,而后将内部属性 0 设置为 p 的外部包裹。

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

一个外部对象仅被用来在内部属性中存储引用。JavaScript 对象不能直接地引用 C++ 对象,因此外部值就像从 JavaScript 到 C++ 的“一座桥梁”。因此外部值是句柄的相反面,由于句柄的做用是让咱们在 C++ 中能够获取 JavaScript 对象的引用。

如下即是 x 的读和写访问器的定义,y 的定义和 x 的十分相似,只需将 x 替换为 y 便可:

void GetPointX(Local<String> property,
                 const PropertyCallbackInfo<Value>& 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_;
    info.GetReturnValue().Set(value);
  }

  void SetPointX(Local<String> property, Local<Value> value,
                 const PropertyCallbackInfo<Value>& 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();
  }

访问器抽象了对于 C++ point 对象的引用和对其的读写操做。这样这些访问器就能够被用于任意数量的被包裹后的 point 对象中了。

拦截器

你还能够在一个脚本访问任意对象属性时,设置一个回调函数。这些回调函数称为拦截器。拦截器分为两种类型:

  • 具名属性拦截器,它会在访问名称为字符串的属性时被调用,如浏览器环境中的 document.theFormName.elementName

  • 索引属性拦截器,它会在访问索引属性时被调用,如浏览器环境中的 document.forms.elements[0]

V8 源码中的 process.cc 文件中,包含了一个拦截器的使用实例。下面例子中的 SetNamedPropertyHandler 设置了 MapGetMapSet 这两个拦截器:

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

MapGet 拦截器源码以下:

void JsHttpRequestProcessor::MapGet(Local<String> name,
                                    const PropertyCallbackInfo<Value>& info) {
  // Fetch the map wrapped by this object.
  map<string, string> *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<string, string>::iterator iter = obj->find(key);

  // If the key is not present return an empty handle as signal.
  if (iter == obj->end()) return;

  // Otherwise fetch the value and wrap it in a JavaScript string.
  const string &value = (*iter).second;
  info.GetReturnValue().Set(String::NewFromUtf8(value.c_str(), String::kNormalString, value.length()));
}

和访问器同样,特定的回调函数会在一个属性被访问后触发。它和访问器的区别就是,访问器会回调仅会在一个特定的属性被访问时触发,而拦截器回调则会在任意属性被访问时触发。

安全模型

“同源策略”(首次出现于网景浏览器 2.0 中),用于阻止从另外一个“源”中加载脚本或文档到本地“源”里。这个源的概念中包含了域名(www.example.com),协议(http 或 https)和端口(如 www.example.com:81 和 www.example.com 不一样源)。以上部分所有同样,才能被视为同源。若是没了这层保护,许多网页就能够会遭到其余恶意网页的攻击。

在 V8 中,“源”即为上下文。在一个上下文中访问另外一个上下文默认是不被容许的。若是必定访问,那么必须使用安全标识(security tokens)或安全回调(security callbacks)。一个安全标识能够是任意类型的值,但一般是一个 symbol 或一个惟一字符串。当你设置一个上下文时,能够经过 SetSecurityToken 可选地设置一个安全标识。若是你没有明确地指明一个安全标识,那么 V8 将会为该上下文自动生成一个。

当试图去访问一个全局变量时,V8 的安全系统首先会去检查被访问的全局变量的上下文的安全标识与访问代码的上下文的安全标识是否一致,若一致,则容许访问。若是安全标识不一致,那么 V8 将会触发一个回调函数来判断这个访问是否该被容许。你能够经过在对象模板的方法 SetAccessCheckCallbacks ,来设置这个安全回调。这个回调的参数为,将会被访问的对象,将会被访问的属性名,和访问的类型(如读,写或删除)而且返回值即表示是否容许此次访问。

在 Google Chrome 中,这套安全机制运用在如下几处:window.focus()window.blur()window.close()window.locationwindow.open()history.forward()history.back()history.go()

异常

当一个错误发生时,V8 将会抛出一个异常。例如,当一个脚本或函数试图去读取一个不存在的属性时,或一个非函数对象被调用时。

若是一次操做失败了,V8 将会返回空句柄。由于在进一步操做前,检查返回值是不是空句柄就变得尤其重要。咱们能够经过本地句柄类(Local)的成员函数 IsEmpty() 来进行检查。

你也能够经过 TryCatch 类捕获异常,例子:

TryCatch trycatch(isolate);
  Local<Value> v = script->Run();
  if (v.IsEmpty()) {
    Local<Value> exception = trycatch.Exception();
    String::Utf8Value exception_str(exception);
    printf("Exception: %s\n", *exception_str);
    // ...
  }

若是返回值是一个空句柄,而且你没有使用 TryCatch ,那么你的代码必需要终止。若是你使用了 TryCatch ,那么你的代码则能够继续执行。

继承

JavaScript 是第一个不基于类的面向对象编程语言。它使用了基于原型的继承。这对于一直使用传统面向对象编程语言(如 C++ 和 Java)的程序员来讲,可能会有些困惑。

传统的面向对象编程语言(如 C++ 和 Java)一般基于两个概念:类和继承。JavaScript 是一个基于原型的编程语言,因此它和传统的面向对象编程语言不一样,它只有对象。JavaScript 并不原生支持基于类声明的继承。可是 JavaScript 的原型机制简化了为实例添加自定义属性和方法的过程。在 JavaScript 中,你能够为单个实例添加自定义的属性。例子:

// 建立一个对象 bicycle
function bicycle(){
}
// 建立一个名为 roadbike 的实例
var roadbike = new bicycle()
// 为 roadbike 定义一个自定义属性 wheels
roadbike.wheels = 2

自定义属性仅仅存在于当前这个实例中。若是咱们建立了另外一个 bicycle 实例,如 mountainbikemountainbike.wheels 将会是 undefined

某些时候,这就是咱们想要的。而又有些时候,咱们想要为全部的实例都添加上这个属性。由于毕竟全部的自行车都有轮子。这是咱们就会使用到原型机制。咱们只需为对象的 prototype 属性上添加咱们想要的自定义属性便可:

// 建立一个对象 bicycle
function bicycle(){
}
// 将 wheels 属性添加到对象的原型上
bicycle.prototype.wheels = 2

这样,全部的 bicycle 实例都将会拥有 wheels 属性。

在 V8 的模板中,作法也是同样的。每个 FunctionTemplate 类实例都有一个 PrototypeTemplate 方法来给出函数的原型。你能够在其上添加属性,为这些属性关联 C++ 函数。都会影响到该模板关联全部的实例中。例子:

Local<FunctionTemplate> biketemplate = FunctionTemplate::New(isolate);
 biketemplate->PrototypeTemplate().Set(
     String::NewFromUtf8(isolate, "wheels"),
     FunctionTemplate::New(isolate, MyWheelsMethodCallback)->GetFunction();
 )

上面的代码将会使全部的 biketemplate 实例拥有一个 wheels 方法。当该方法被调用时,C++ 函数 MyWheelsMethodCallback 就会执行。

V8 的 FunctionTemplate 类提供了一个公开成员函数 Inherit() ,当你想要一个函数模板继承于另外一个函数模板时,你可使用它,例子:

void Inherit(Local<FunctionTemplate> parent);

最后

原文连接:https://developers.google.com/v8/embed

相关文章
相关标签/搜索