此文档介绍了V8的一些关键概念,并提供了一个“ hello world”示例来开始使用V8代码。

V8文档

详细的v8文档接口请参考:https://v8docs.nodesource.com/

Audience

本文档适用于希望将V8 JavaScript引擎嵌入C ++应用程序中的C ++程序员。它可以帮助您使自己的应用程序的C ++对象和方法可用于JavaScript,并使JavaScript对象和函数可用于C ++应用程序。

Hello world

让我们看一个 Hello World 示例,该示例将JavaScript语句作为字符串参数,将其作为JavaScript代码执行,然后将结果打印到标准输出中:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include "include/libplatform/libplatform.h"
  5. #include "include/v8.h"
  6. int main(int argc, char* argv[]) {
  7. // Initialize V8.
  8. v8::V8::InitializeICUDefaultLocation(argv[0]);
  9. v8::V8::InitializeExternalStartupData(argv[0]);
  10. std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  11. v8::V8::InitializePlatform(platform.get());
  12. v8::V8::Initialize();
  13. // Create a new Isolate and make it the current one.
  14. v8::Isolate::CreateParams create_params;
  15. create_params.array_buffer_allocator =
  16. v8::ArrayBuffer::Allocator::NewDefaultAllocator();
  17. v8::Isolate* isolate = v8::Isolate::New(create_params);
  18. {
  19. v8::Isolate::Scope isolate_scope(isolate);
  20. // Create a stack-allocated handle scope.
  21. v8::HandleScope handle_scope(isolate);
  22. // Create a new context.
  23. v8::Local<v8::Context> context = v8::Context::New(isolate);
  24. // Enter the context for compiling and running the hello world script.
  25. v8::Context::Scope context_scope(context);
  26. // Create a string containing the JavaScript source code.
  27. v8::Local<v8::String> source =
  28. v8::String::NewFromUtf8(isolate, "'Hello' + ', World!'",
  29. v8::NewStringType::kNormal)
  30. .ToLocalChecked();
  31. // Compile the source code.
  32. v8::Local<v8::Script> script =
  33. v8::Script::Compile(context, source).ToLocalChecked();
  34. // Run the script to get the result.
  35. v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
  36. // Convert the result to an UTF8 string and print it.
  37. v8::String::Utf8Value utf8(isolate, result);
  38. printf("%s\n", *utf8);
  39. }
  40. // Dispose the isolate and tear down V8.
  41. isolate->Dispose();
  42. v8::V8::Dispose();
  43. v8::V8::ShutdownPlatform();
  44. delete create_params.array_buffer_allocator;
  45. return 0;
  46. }

首先,一些关键概念:

  • An isolate is a VM instance with its onw heap(隔离是具有自己的堆的VM实例);
  • A local handle is a pointer to an object. All V8 objects are accessed using handles. They are necessary because of the way the V8 garbage collector works.(Local handle是指向对象的指针。使用handle可以访问所有V8对象。由于V8垃圾收集器的工作方式,它们是必需的)
  • A handle scope can be thought of as a container for any number of handles.When you’ve finished with your handles, instead of deleting each one individually you can simply delete their scope.(可以将句柄作用域视为任意数量的句柄的容器。完成句柄处理后,不必删除单个的句柄,只需删除它们的作用域即可)
  • A context is an execution environment that allows separate, unrelated, JavaScript code to run in a single instance of V8. You must explicitly specify the context in which you want any JavaScript code to be run.(上下文是一种执行环境,它允许单独的,不相关的JavaScript代码在V8的单个实例中运行。您必须明确指定要在其中运行任何JavaScript代码的上下文)

这些概念将在高级指南中详细讨论。

Run the example

detail

Advanced guide

现在,您已经熟悉了将V8用作独立虚拟机以及一些重要的V8概念,例如handles(句柄),scopes(作用域)和context(上下文),让我们进一步讨论这些概念,并介绍一些其他概念,这些概念对于将V8嵌入您自己的C ++应用程序至关重要。

V8 API提供了用于编译和执行脚本,访问C ++方法和数据结构,处理错误以及启用安全检查的功能。您的应用程序可以像任何其他C ++库一样使用V8。您的C ++代码通过包含标头 include/v8.h ,通过V8 API访问V8。

handles和垃圾收集

句柄提供对JavaScript对象在堆中位置的引用。 V8垃圾收集器回收了无法再访问的对象所使用的内存。 在垃圾收集过程中,垃圾收集器通常将对象移动到堆中的不同位置。 当垃圾收集器移动对象时,垃圾收集器还会使用对象的新位置来更新所有引用该对象的句柄。

如果无法从JavaScript访问对象并且没有引用该对象的句柄,则该对象被视为垃圾。 垃圾收集器会不时删除所有被视为垃圾的对象。 V8的垃圾回收机制是V8性能的关键。

有几种类型的句柄:

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

本地句柄类:Local <SomeType>

注意:handle堆栈不是C ++调用堆栈的一部分,但 handle scope 已嵌入C ++堆栈中。 处理范围只能是堆 栈分配的,不能与new一起分配。

  1. 持久 handle 提供对堆分配的JavaScript对象的引用,就像 Local handle 一样。有两种类型,它们处理的引用的生存期管理不同。当需要为多个函数调用保留对一个对象的引用时,或者当 handle 寿命不对应于C ++范围时,请使用持久句柄。例如,谷歌浏览器使用持久性句柄来引用文档对象模型(DOM)节点。 当对对象的唯一引用来自弱持久句柄时,可以使用 PersistentBase::SetWeak 将持久句柄设为弱,以从垃圾回收器触发回调。
    1. UniquePersistent<SomeType> handle 依靠C ++构造函数和析构函数来管理基础对象的生存期。
    2. Persistent<SomeType> 可以使用其构造函数进行构造,但是必须使用 Persistent::Reset 明确清除。
  2. 还有一些其他类型的句柄很少使用,这里我们仅简要提及:
    1. Eternal 是用于永远不会被删除的JavaScript对象的持久句柄。 使用起来更便宜,因为它使垃圾收集器不必确定该对象的活动性。
    2. PersistentUniquePersistent 都无法复制,这使其不适用于C ++ 11之前的标准库容器中的值。 PersistentValueMapPersistentValueVector 提供持久性值的容器类,具有映射和类似于矢量的语义。

当然,每次创建对象时创建 Local handle 都会导致很多 handle !这是 handle scopes 非常有用的地方。您可以将 handle scopes 视为包含大量句柄的容器。调用 handle scopes 的析构函数时,将从堆栈中删除在该范围内创建的所有句柄。如您所料,这将导致句柄指向的对象可以被垃圾收集器从堆中删除。

在下图中,您可以看到 handle-stackheap-allocated(堆分配) 的对象。请注意,Context::New() 返回一个 Local handle ,并基于它创建一个新的 Persistent handle ,以演示Persistent handle 的用法。
image.png
调用析构函数 HandleScope::〜HandleScope 时,将删除句柄范围。 如果没有其他引用,则在删除的 handle scopes 内由句柄引用的对象可以在下一个垃圾回收中删除。 垃圾回收器还可以从堆中删除source_objscript_obj 对象,因为它们不再被任何句柄引用或可以通过JavaScript访问。 由于context handle 是持久句柄,因此退出句柄作用域时不会将其删除。 删除 context handle 的唯一方法是在其上显式调用 Reset

注意此模型的一个常见陷阱很重要:您不能直接从声明句柄作用域的函数中返回本地句柄。 如果您执行本地句柄,则尝试返回的结果最终会在函数返回之前立即被句柄作用域的析构函数删除。 返回本地句柄的正确方法是构造一个 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.
  v8::EscapableHandleScope handle_scope(isolate);

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

  // Return an empty result if there was an error creating the array.
  if (array.IsEmpty())
    return v8::Local<v8::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 方法将其参数的值复制到封闭范围内,删除其所有本地句柄,然后返回可以安全返回的新句柄副本。

Contexts

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

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

在CPU时间和内存方面,鉴于必须构建的内置对象的数量,创建新的执行上下文似乎是一项昂贵的操作。但是,V8的广泛缓存确保了,尽管您创建的第一个 context 有些昂贵,但随后的 context要便宜得多。这是因为第一个 context 需要创建内置对象并解析内置JavaScript代码,而后续 context 仅需要为其 context 创建内置对象。 借助V8快照功能(通过默认的构建选项 snapshot=yes 激活),创建第一个 context 所花费的时间将得到高度优化,因为快照包括一个序列化堆,该堆包含已为内置JavaScript代码编译的代码。 除了垃圾回收,V8的广泛缓存也是V8性能的关键。

创建 context 后,可以多次输入和退出它。 在 contextA 中时,您还可以输入其他 contextB ,这意味着您将B替换为当前 contextA 。退出B时,A将还原为当前 context 。 如下图所示:
image.png
请注意,每个 context 的内置实用程序功能和对象保持独立。创建 context 时,可以选择设置安全令牌。有关更多信息,请参见安全模型部分。

在V8中使用 context 的动机是使浏览器中的每个窗口和iframe可以拥有自己的全新JavaScript环境。

Templates

模板是上下文中JavaScript函数和对象的蓝图。您可以使用模板将C ++函数和数据结构包装在JavaScript对象中,以便可以由JavaScript脚本操纵它们。例如,谷歌浏览器使用模板将C ++ DOM节点包装为JavaScript对象,并在全局名称空间中安装函数。您可以创建一组模板,然后对创建的每个新上下文使用相同的模板。您可以根据需要拥有任意数量的模板。但是,在任何给定的上下文中,您只能拥有任何模板的一个实例。

在JavaScript中,函数和对象之间具有很强的对偶性。要使用Java或C ++创建新的对象类型,通常需要定义一个新类。在JavaScript中,您可以创建一个新函数,然后使用该函数作为构造函数来创建实例。 JavaScript对象的布局和功能与构造它的功能紧密相关。 V8模板的工作方式反映了这一点。有两种类型的模板:

  • Function templates

Function tempaltes 是单个功能的蓝图。 通过在要实例化JavaScript函数的 context 中调用模板的GetFunction 方法,可以创建模板的JavaScript实例。 您还可以将 C++ 回调与函数模板相关联,该函数模板在调用JavaScript函数实例时被调用

  • Object templates

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

  • 当脚本访问特定的对象属性时,将调用访问器回调
  • 脚本访问任何对象属性时,将调用拦截器回调

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

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

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

此示例代码取自 process.cc 示例中的 JsHttpProcessor::Initializer

Accessors

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

访问器的复杂性取决于您要处理的数据类型:

访问静态全局变量

假设有两个C ++整数变量x和y作为 context 中的全局变量可供JavaScript使用。 为此,每当脚本读取或写入这些变量时,就需要调用C ++访问器(Accessors)函数。 这些访问器函数使用 Integer::New 将 C++ 整数转换为JavaScript整数,并使用 Int32Value 将JavaScript整数转换为C ++整数。 下面提供了一个示例:

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

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

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

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

请注意,上面代码中的 object template 是与 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 包装器对象创建一个对象模板:

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

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

point_templ->SetInternalFieldCount(1);

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

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

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

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

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

外部对象只是围绕 void* 的包装。外部对象只能用于将参考值存储在内部字段中。
JavaScript对象不能直接引用 C++ 对象,因此 external value 用作从JavaScript到C++的“bridge”。 从这种意义上说,外部值与句柄相反,因为句柄使 C++ 可以引用JavaScript对象。

这是x的 get 和 set 访问器的定义,y的访问器定义相同,只是y替换了x:

void GetPointX(Local<String> property,
               const PropertyCallbackInfo<Value>& info) {
  v8::Local<v8::Object> self = info.Holder();
  v8::Local<v8::External> wrap =
      v8::Local<v8::External>::Cast(self->GetInternalField(0));
  void* ptr = wrap->Value();
  int value = static_cast<Point*>(ptr)->x_;
  info.GetReturnValue().Set(value);
}

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

访问器提取对由JavaScript对象包装的 Point 对象的引用,然后读取和写入关联的字段。这样,这些通用访问器可以在任意数量的包装 point 对象上使用。

Interceptors

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

  • 命名属性拦截器-在访问具有字符串名称的属性时调用。
    • 在浏览器环境中,此示例为 document.theFormName.elementName
  • 索引属性拦截器-访问索引属性时调用。在浏览器环境中,此示例为document.forms.elements [0]

V8源代码提供的样本 process.cc 包含使用拦截器的示例。在以下代码段中,SetNamedPropertyHandler 指定了 MapGetMapSet 拦截器:

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

The MapGet interceptor is provided below:

void JsHttpRequestProcessor::MapGet(v8::Local<v8::String> name,
                                    const v8::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(v8::String::NewFromUtf8(
      value.c_str(), v8::String::kNormalString, value.length()));
}

与访问器一样,只要访问属性,就会调用指定的回调。访问器和拦截器之间的区别在于,拦截器处理所有属性,而访问器则与一个特定的属性相关联。

Security model

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

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

尝试访问全局变量时,V8安全系统首先将尝试访问全局对象的代码的安全令牌与正在访问的全局对象的安全令牌进行比较。如果令牌匹配,则授予访问权限。如果令牌不匹配,则V8执行回调以检查是否应允许访问。您可以使用对象模板上的SetAccessCheckCallbacks方法,通过在对象上设置安全性回调来指定是否应允许访问对象。然后,V8安全系统可以获取正在访问的对象的安全回调,并调用它以询问是否允许另一个上下文访问它。给该回调函数提供要访问的对象,要访问的属性的名称,访问的类型(例如,读取,写入或删除),并返回是否允许访问。

此机制在Google Chrome中实现,因此,如果安全令牌不匹配,则使用特殊的回调仅允许访问以下内容:window.focus(),window.blur(),window.close(),window.location ,window.open(),history.forward(),history.back()和history.go()。

Exceptions

如果发生错误,例如,当脚本或函数试图读取不存在的属性,或者调用的函数不是函数时,V8会引发异常。

如果操作未成功,则V8返回空句柄。 因此,在继续执行之前,代码必须检查返回值不是空句柄,这一点很重要。 使用Local类的公共成员函数IsEmpty()检查空句柄。

您可以使用TryCatch捕获异常,例如:

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

如果返回的值是一个空句柄,并且您没有适当的TryCatch,则您的代码必须bail out。如果确实有TryCatch,则会捕获异常,并允许您的代码继续处理。

Inheritance

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;

以这种方式添加的自定义属性仅在该对象的实例中存在。 如果我们创建 bicycle() 的另一个实例,例如称为mountainbike,则除非明确添加了wheels属性,否则mountainbike.wheels将返回undefined。

有时这正是所需的,而在其他时候,将custom属性添加到对象的所有实例将很有帮助-毕竟,所有自行车都有轮子。 这是JavaScript原型对象非常有用的地方。 要使用原型对象,请在对象上引用关键字prototype,然后向其添加自定义属性,如下所示:

// First, create the “bicycle” object
function bicycle() {}
// Assign the wheels property to the object’s prototype
bicycle.prototype.wheels = 2;

现在,bicycle()的所有实例都将预先内置了wheel属性。

在带有模板的V8中使用了相同的方法。 每个 FunctionTemplate 都有一个 PrototypeTemplate 方法,该方法为该函数的原型提供了一个模板。 您可以在 PrototypeTemplate 上设置属性并将 C++ 函数与这些属性相关联,然后该 PrototypeTemplate 将出现在相应 FunctionTemplate 的所有实例上。 例如:

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

这将导致 biketemplate 的所有实例在其原型链中都有一个 wheel 方法,该方法在被调用时将导致C ++函数MyWheelsMethodCallback 被调用。
V8的 FunctionTemplate 类提供了公共成员函数 Inherit(),当您希望一个功能模板从另一个功能模板继承时可以调用该函数,如下所示:

void Inherit(v8::Local<v8::FunctionTemplate> parent);

引用

参考:https://v8.dev/docs/embed#run-the-example