该文翻译自Advanced guide

Changelog: v1.0.0 2019.07.23

现在你已经熟悉了如何将v8作为一个独立的虚拟机器来使用,以及v8的一些重要概念,比如handlesscopescontext。接下去让我们继续深入讨论这些概念并介绍其他几个概念,这些概念对于内嵌V8到你自己的C++应用程序是很重要的。

V8 API提供了函数来编译和执行脚本,访问C++的方法以及数据结构,处理错误和使能安全性检查。你的应用可以像其他C++库一样用V8。你的C++代码通过引用include/v8.h头文件来访问V8的所有API。

1、句柄和垃圾回收

句柄提供了Javascript在堆中位置的一个索引。V8垃圾回收器回收那些不再能够被访问的对象所使用的内存。在垃圾回收过程中,垃圾回收器经常将对象移动到堆中的不同位置。当垃圾回收器移动一个对象时,垃圾回收器会更新引用到该对象的所有句柄指向新的位置。

如果一个对象在JavaScript中不能再访问,并且没有引用它的句柄,那么它就被认为是可回收的。垃圾回收器会不时地删除所有被认为是可回收的对象。V8的垃圾回收机制是V8性能的关键。

下面有几种类型的句柄:

  1. 本地句柄保存在一个栈中并且当其对应的析构器被调用的时候就会被删除。这些句柄的生命周期由其句柄范围决定,句柄范围通常在函数调用开始时创建。当句柄范围被删除时,垃圾回收器可以自由地释放句柄范围中句柄以前引用的那些对象,前提是它们不再可以从JavaScript或其他句柄被访问。在上面的hello world示例中使用了这种类型的句柄。

本地句柄的类名是:Local<SomeType>

注意:句柄的栈并不属于C++调用栈的一部分,但是句柄范围是嵌入在C++栈中的。句柄范围只能是栈分配空间,而不能使用new进行分配内存。

  1. 持久句柄提供对堆分配的JavaScript对象的引用,就像本地句柄一样。有两种方式,它们在处理引用的生命周期管理方面有所不同。当您需要为多个函数调用保存对对象的引用时,或者当句柄生存期不对应于C++范围时,请使用持久句柄。例如,谷歌Chrome使用持久句柄引用文档对象模型(DOM)节点。可以使用PersistentBase::SetWeak将持久句柄设置为弱句柄,以便在对对象的惟一引用来自弱持久句柄时触发垃圾回收器的回调。

    UniquePersistent<SomeType>句柄依赖于C++构造器和析构器,用来管理底层对象的生命周期

    Persistent<SomeType>可以使用自己的构造器进行构造,但是必须明确使用Persistent::Reset进行清除。

  2. 还有一些其他类型的句柄很少使用,我们在这里只简要地提一下:

    Eternal是一个JavaScript对象的持久句柄,该对象永远不会被删除。它的使用成本更低,因为它使垃圾回收器不必去猜测对象是否还在使用。 因为PersistentUniquePersistent都不能被复制,这使得它们不适合作为pre-C++ 11标准库容器的值。PersistentValueMapPersistentValueVector为持久值提供了容器类,它们具有映射和类似向量的语义。C++ 11嵌入器不需要这些,因为C++ 11的move语义解决了底层问题。

当然,每次创建对象时都创建一个本地句柄会产生很多句柄!这个时候句柄范围(handle scope)就显得很有用了。您可以将句柄范围看作是包含许多句柄的容器。当调用句柄范围的析构函数时,在该范围内创建的所有句柄都将从堆栈中删除。正如您所期望的,这将导致句柄指向的对象都有可能被垃圾回收器从堆中删除。

回到我们最简单的hello world实例,下面一图你可以看到句柄栈和堆分配的对象。值得注意的是Context:New()返回的是本地句柄,我们基于它创建一个新的持久化句柄来表示对Persisten句柄的使用:

HandleScope::~HandleScope解构器被调用的时候,句柄范围就被删除了。如果没有对句柄的其他引用,则在已删除句柄范围内的句柄引用的对象有资格在下一个垃圾回收中删除。垃圾回收器还可以从堆中删除source_obj和script_obj对象,因为它们不再被任何句柄引用,也不再被JavaScript访问。由于上下文句柄是一个持久句柄,所以在退出句柄范围时不会删除它。删除上下文句柄的唯一方法是显式地对其调用Reset。

Note 在本文档中,“句柄”一词指的是本地句柄。在讨论持久句柄时,我们会称呼其全称。

注意此模型有一个常见缺陷:不能直接从声明句柄范围的函数返回本地句柄。如果您这样做了,那么你尝试要返回的本地句柄会在函数返回之前立即被句柄作用域的析构函数删除。返回本地句柄的正确方法是构造一个EscapableHandleScope而不是HandleScope,并在句柄范围上调用Escape方法,传入您希望返回值的句柄。下面是一个实际操作的例子:

// This function returns a new array with three elements, x, y, and z.
Local<Array> NewPointArray(int x, int y, int z) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();

  // We will be creating temporary handles so we use a handle scope.
  EscapableHandleScope handle_scope(isolate);

  // Create a new empty array.
  Local<Array> array = Array::New(isolate, 3);

  // Return an empty result if there was an error creating the array.
  if (array.IsEmpty())
    return Local<Array>();

  // Fill out the values
  array->Set(0, Integer::New(isolate, x));
  array->Set(1, Integer::New(isolate, y));
  array->Set(2, Integer::New(isolate, z));

  // Return the value through Escape.
  return handle_scope.Escape(array);
}

Escape方法将其参数的值复制到封闭范围中,删除它的所有本地句柄,然后返回可以安全返回的新句柄副本

2、Contexts

在V8中,上下文是一个执行环境,它允许单独的、不相关的JavaScript应用程序在V8的一个实例中运行。您必须显式地指定要在其中运行任何JavaScript代码的上下文。

为什么这是必要的?因为JavaScript提供了一组内置的实用函数和对象,这些都是可以通过JavaScript代码进行更改。例如,如果两个完全不相关的JavaScript函数都以相同的方式更改了全局对象,那么很可能会出现意想不到的结果。

就CPU时间和内存而言,考虑到必须构建的内置对象的数量,创建一个新的执行上下文似乎是一个昂贵的操作。然而,V8的大量缓存确保在创建第一个昂贵的上下文之后,后续上下文创建将会变得省时省力。这是因为第一个上下文需要创建内置对象并解析内置JavaScript代码,而后续上下文只需要为其上下文创建内置对象。使用V8快照特性(使用build选项snapshot=yes激活,这是默认值),创建第一个上下文所花费的时间将得到高度优化,因为快照包含一个序列化的堆来容纳已编译的内置JavaScript代码。除了垃圾回收,V8的大量缓存也是V8性能的关键。

当您创建了一个上下文时,您可以多次进入和退出它。当您在上下文A中,您还可以进入一个不同的上下文B,这意味着您可以用B替换A作为当前上下文。当你退出B的时候,A就恢复为当前的上下文,如下图所示:

注意,每个上下文的内置实用程序函数和对象是分开的。创建上下文时,可以选择设置安全令牌。有关更多信息,请参见安全模型部分。

V8中使用上下文的动机是为了让浏览器中的每个窗口和iframe都有自己的新JavaScript环境。

3、Templates

模板是上下文中JavaScript函数和对象的一种模型。您可以使用模板将C++函数和数据结构封装在JavaScript对象中,以便JavaScript脚本可以操作它们。例如,谷歌Chrome使用模板将C++ DOM节点封装为JavaScript对象,并在全局命名空间中挂载函数。您可以创建一组模板,然后为创建的每个新上下文使用相同的模板。您可以拥有任意多的模板。但是,在任何给定上下文中只能有一个模板实例。

在JavaScript中,函数和对象之间有很强的对偶性。要在Java或C++中创建新类型的对象,通常需要定义一个新类。而在JavaScript中,你却会创建一个新函数,并使用该函数作为构造函数创建实例。JavaScript对象的布局和功能与构造它的函数密切相关。这反映在V8模板的工作方式上。有两种类型的模板:

函数模板

函数模板是单个函数的模型。通过在你想实例化JavaScript函数的上下文中调用模板的GetFunction方法,可以创建模板的JavaScript实例。您还可以将C++回调与函数模板关联起来,函数模板在调用JavaScript函数实例时被调用。

对象模板

每个函数模板都有一个关联的对象模板。这用于配置使用此函数作为构造函数创建的对象。您可以将两种类型的C++回调与对象模板关联起来:

当脚本访问特定对象属性时调用访问器的回调

当脚本访问对象任何属性时拦截器的回调

Accessorsinterceptors在接下去的章节中会介绍

下面的代码提供了为全局对象创建模板和设置内置全局函数的示例。

// Create a template for the global object and set the
// built-in global functions.
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(String::NewFromUtf8(isolate, "log"),
            FunctionTemplate::New(isolate, LogCallback));

// Each processor gets its own context so different processors
// do not affect each other.
Persistent<Context> context = Context::New(isolate, NULL, global);

此示例代码取自process.cc样例中的JsHttpProcessor::Initializer方法。

3、访问器

accessor是一个C++回调函数,当JavaScript脚本访问对象属性时,它计算并返回一个值。访问器是通过使用SetAccessor方法的对象模板配置的。此方法接受与其关联的属性的名称,并在脚本尝试读取或写入该属性时运行两个回调。

访问器的复杂性取决于您操作的数据类型:

Accessing static global variables
Accessing dynamic variables

3.1、访问静态全局变量

假设有两个C++整数变量,x和y, 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<void>& info) {
  x = value->Int32Value();
}

// YGetter/YSetter are so similar they are omitted for brevity

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);

注意,上面代码中的对象模板是与上下文同时创建的。模板可以预先创建,然后用于任意数量的上下文。

3.2、访问动态变量

在前面的示例中,变量是静态的和全局的。如果所操作的数据是动态的,就像浏览器中的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包装器对象创建一个对象模板:

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

每个JavaScriptpoint对象都保存对C++对象的引用,该C++对象是一个带有内部字段的包装器。之所以这样命名这些字段,是因为它们不能从JavaScript中访问,只能从C++代码中访问。一个对象可以有任意数量的内部字段,在对象模板上设置的内部字段数量如下:

point_templ->SetInternalFieldCount(1);

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

将x和y访问器添加到模板:

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

接下来,通过创建模板的一个新实例来包装一个C++point,然后将内部字段0设置为围绕point的外部包装器。

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

外部对象只是一个围绕void*的包装器。外部对象只能用于在内部字段中存储引用值。JavaScript对象不能直接引用C++对象,因此外部值被用作从JavaScript到C++的“桥梁”。从这个意义上说,外部值与句柄相反,因为句柄允许C++引用JavaScript对象。

下面是x的getset访问器的定义,y的访问器定义是一样的,只是y替换了x:

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<void>& 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对象。

4、拦截器

您还可以为脚本访问任何对象属性时指定回调。这些被称为拦截器。为了提高效率,有两种拦截器:

命名属性拦截器——在访问具有字符串名称的属性时调用。 在浏览器环境中,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()));
}

与访问器一样,无论何时访问属性,都会调用指定的回调函数。访问器和拦截器的区别在于拦截器处理所有属性,而访问器只会关联某个特定的属性。

5、安全模型

“同源策略”(Netscape Navigator 2.0首次引入)防止从一个“源”加载的文档或脚本获取或设置来自另一个“源”的文档属性。术语origin在这里定义为域名(例如www.example.com)、协议(例如https)和端口的组合。例如,www.example.com:81与www.example.com不是同源的。这三个必须匹配两个,这样web页面才能被认为具有相同的origin。如果没有这种保护,恶意web页面可能会损害另一个web页面的完整性。

在V8中,“origin”被定义为上下文。默认情况下,不允许访问您当前调用的上下文之外的任何上下文。要访问您当前调用的上下文之外的上下文,您需要使用安全令牌或安全回调。安全令牌可以是任何值,但通常是一个符号,一个在其他任何地方都不存在的规范字符串。在设置上下文时,可以选择使用SetSecurityToken指定安全令牌。如果您没有指定安全令牌,V8将自动为您创建的上下文生成一个。

当试图访问全局变量时,V8安全系统首先检查正在访问的全局对象的安全令牌与试图访问全局对象的代码的安全令牌。如果令牌匹配,则授予访问权限。如果令牌与V8不匹配,则执行回调以检查是否应该允许访问。你可以使用对象模板上的SetAccessCheckCallbacks方法在对象上设置安全回调,从而指定是否应该允许访问对象。V8安全系统可以获取被访问对象的安全回调,并调用它来询问是否允许其他上下文访问它。这个回调函数给出要访问的对象、要访问的属性的名称、访问的类型(例如读、写或删除),并返回是否允许访问。

这个机制是在谷歌Chrome中已经实现,因此,如果安全令牌不匹配,则使用一个特殊的回调函数只允许访问以下内容:window.focus()window.blur()window.close()window.locationwindow.open()history.forward()history.back()history.go()

6、异常

V8在出现错误时抛出异常——例如,当脚本或函数试图读取不存在的属性时,或者调用的函数不是函数时。

如果操作没有成功,V8将返回一个空句柄。因此,在继续执行之前,代码检查返回值是否为空句柄非常重要。使用本地类的公共成员函数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捕获异常,你的代码被允许继续执行。

7、继承

JavaScript是一种无类、面向对象的语言,因此,它使用原型继承而不是经典继承。对于使用C++和Java等传统面向对象语言的程序员来说,这可能会让他们感到困惑。

基于类的面向对象语言,如Java和C++,建立在两个不同实体的概念之上:类和实例。JavaScript是一种基于原型的语言,因此不做这种区分:它只有对象。JavaScript本身并不支持类层次结构的声明;然而,JavaScript的原型机制简化了向对象的所有实例添加自定义属性和方法的过程。在JavaScript中,可以向对象添加自定义属性。例如:

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

以这种方式添加的自定义属性只存在于该对象的实例中。如果我们创建了bike()的另一个实例,例如,mountainbike。除非显式添加了wheels属性,否则mountainbike.wheels将返回未定义的。

有时这正是需要的,但有时将自定义属性添加到对象的所有实例中会很有帮助——毕竟所有自行车都有轮子。这就是JavaScript的原型对象非常有用的地方。要使用prototype对象,在添加自定义属性之前,先引用对象上的关键字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的所有实例上。例如:

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);