V8 嵌入入门

本文档介绍了一些关键的 V8 概念,并提供了一个“Hello World”示例,帮助您开始使用 V8 代码。

受众 #

本文档面向希望在 C++ 应用程序中嵌入 V8 JavaScript 引擎的 C++ 程序员。它帮助您将自己的应用程序的 C++ 对象和方法提供给 JavaScript,并将 JavaScript 对象和函数提供给您的 C++ 应用程序。

Hello World #

让我们看一下 Hello World 示例,它将 JavaScript 语句作为字符串参数,将其作为 JavaScript 代码执行,并将结果打印到标准输出。

首先,一些关键概念

这些概念在 高级指南 中有更详细的讨论。

运行示例 #

按照以下步骤自己运行示例

  1. 按照 Git 指令 下载 V8 源代码。

  2. 此 Hello World 示例的指令最后一次在 V8 v11.9 中测试。您可以使用 git checkout branch-heads/11.9 -b sample -t 检查此分支。

  3. 使用辅助脚本创建构建配置

    tools/dev/v8gen.py x64.release.sample

    您可以通过运行以下命令检查和手动编辑构建配置

    gn args out.gn/x64.release.sample
  4. 在 Linux 64 系统上构建静态库

    ninja -C out.gn/x64.release.sample v8_monolith
  5. 编译 hello-world.cc,链接到构建过程中创建的静态库。例如,在 64 位 Linux 上使用 GNU 编译器

    g++ -I. -Iinclude samples/hello-world.cc -o hello_world -fno-rtti -lv8_monolith -lv8_libbase -lv8_libplatform -ldl -Lout.gn/x64.release.sample/obj/ -pthread -std=c++17 -DV8_COMPRESS_POINTERS -DV8_ENABLE_SANDBOX
  6. 对于更复杂的代码,V8 在没有 ICU 数据文件的情况下会失败。将此文件复制到您的二进制文件存储位置

    cp out.gn/x64.release.sample/icudtl.dat .
  7. 在命令行中运行 hello_world 可执行文件。例如,在 Linux 上,在 V8 目录中运行

    ./hello_world
  8. 它打印 Hello, World!。耶!

如果您正在寻找与主分支同步的示例,请查看文件 hello-world.cc。这是一个非常简单的示例,您可能希望做的不仅仅是将脚本作为字符串执行。 下面的高级指南 包含有关 V8 嵌入器的更多信息。

更多示例代码 #

以下示例作为源代码下载的一部分提供。

process.cc #

此示例提供了扩展假设的 HTTP 请求处理应用程序(例如,可能是 Web 服务器的一部分)所需的代码,使其可脚本化。它将 JavaScript 脚本作为参数,该脚本必须提供一个名为 Process 的函数。JavaScript Process 函数可用于例如收集信息,例如虚拟 Web 服务器提供的每个页面的点击次数。

shell.cc #

此示例将文件名作为参数,然后读取并执行其内容。包括一个命令提示符,您可以在其中输入要执行的 JavaScript 代码片段。在此示例中,还通过使用对象和函数模板向 JavaScript 添加了 print 等其他函数。

高级指南 #

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

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

句柄和垃圾收集 #

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

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

句柄有几种类型

当然,每次创建对象时都创建一个本地句柄会导致大量句柄!这就是句柄范围非常有用的地方。您可以将句柄范围视为一个包含大量句柄的容器。当调用句柄范围的析构函数时,在该范围内创建的所有句柄都将从堆栈中删除。正如您所料,这会导致句柄指向的对象在下次垃圾收集时有资格从堆中删除,前提是它们没有其他引用。

回到 我们非常简单的 Hello World 示例,在下图中,您可以看到句柄堆栈和堆分配的对象。请注意,Context::New() 返回一个 Local 句柄,我们根据它创建一个新的 Persistent 句柄来演示 Persistent 句柄的使用。

当析构函数 HandleScope::~HandleScope 被调用时,句柄范围被删除。被删除的句柄范围内的句柄引用的对象有资格在下一次垃圾收集中被删除,前提是它们没有其他引用。垃圾收集器还可以从堆中删除 source_objscript_obj 对象,因为它们不再被任何句柄引用或无法从 JavaScript 访问。由于上下文句柄是持久句柄,因此在退出句柄范围时不会删除它。删除上下文句柄的唯一方法是显式调用其上的 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 方法将参数的值复制到封闭范围,删除其所有本地句柄,然后返回新的句柄副本,该副本可以安全地返回。

上下文 #

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

为什么需要这样做?因为 JavaScript 提供了一组内置的实用程序函数和对象,这些函数和对象可以被 JavaScript 代码更改。例如,如果两个完全不相关的 JavaScript 函数都以相同的方式更改了全局对象,那么很可能发生意外结果。

在 CPU 时间和内存方面,考虑到必须构建的内置对象数量,创建新的执行上下文似乎是一个昂贵的操作。但是,V8 的广泛缓存确保,虽然您创建的第一个上下文有点昂贵,但后续上下文要便宜得多。这是因为第一个上下文需要创建内置对象并解析内置 JavaScript 代码,而后续上下文只需要为其上下文创建内置对象。使用 V8 快照功能(使用构建选项 snapshot=yes 激活,这是默认选项),创建第一个上下文所花费的时间将得到高度优化,因为快照包含一个序列化堆,其中包含已编译的内置 JavaScript 代码的代码。除了垃圾收集之外,V8 的广泛缓存也是 V8 性能的关键。

创建上下文后,您可以进入和退出它任意多次。当您处于上下文 A 中时,您也可以进入另一个上下文 B,这意味着您将 A 替换为当前上下文 B。当您退出 B 时,A 将被恢复为当前上下文。如下所示

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

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

模板 #

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

在 JavaScript 中,函数和对象之间存在着强烈的二元性。在 Java 或 C++ 中创建一种新的对象类型,通常需要定义一个新的类。在 JavaScript 中,您会创建一个新的函数,并使用该函数作为构造函数来创建实例。JavaScript 对象的布局和功能与其构造它的函数紧密相关。这反映在 V8 模板的工作方式中。有两种类型的模板

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

// 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

访问器 #

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

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

访问静态全局变量 #

假设有两个 C++ 整型变量 xy,它们将在上下文中作为全局变量提供给 JavaScript。为此,您需要在脚本读取或写入这些变量时调用 C++ 访问器函数。这些访问器函数使用 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);

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

访问动态变量 #

在前面的示例中,变量是静态的和全局的。如果正在操作的数据是动态的,就像浏览器中的 DOM 树一样,该怎么办?假设 xy 是 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++ 对象。

xy 访问器添加到模板中

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

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

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

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

以下是 xgetset 访问器的定义,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 对象的引用,然后读取和写入关联的字段。这样,这些通用访问器就可以用于任意数量的包装点对象。

拦截器 #

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

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

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

MapGet 拦截器如下所示

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

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

安全模型 #

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

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

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

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

异常 #

如果发生错误,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,则您的代码必须退出。如果您使用了 TryCatch,则会捕获异常,并且您的代码可以继续处理。

继承 #

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,则 mountainbike.wheels 将返回 undefined,除非显式添加 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 方法,该方法提供函数原型的模板。您可以在 PrototypeTemplate 上设置属性,并将 C++ 函数与这些属性关联,这些属性随后将出现在相应 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 的所有实例在其原型链中都具有一个 wheels 方法,该方法在被调用时会导致调用 C++ 函数 MyWheelsMethodCallback

V8 的 FunctionTemplate 类提供了公共成员函数 Inherit(),当您希望一个函数模板从另一个函数模板继承时,可以调用该函数,如下所示

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