V8 Torque 内建函数

本文档旨在作为编写 Torque 内建函数的入门指南,面向 V8 开发人员。Torque 替代 CodeStubAssembler 成为实现新内建函数的推荐方式。有关此指南的 CSA 版本,请参阅 CodeStubAssembler 内建函数

内建函数 #

在 V8 中,内建函数可以看作是在运行时由 VM 执行的代码块。一个常见的用例是实现内建对象的函数(例如 RegExpPromise),但内建函数也可以用于提供其他内部功能(例如作为 IC 系统的一部分)。

V8 的内建函数可以使用多种不同的方法实现(每种方法都有不同的权衡)

本文档的其余部分将重点介绍后者,并简要介绍如何开发一个暴露给 JavaScript 的简单 Torque 内建函数。有关 Torque 的更完整信息,请参阅 V8 Torque 用户手册

编写 Torque 内建函数 #

在本节中,我们将编写一个简单的 CSA 内建函数,它接受一个参数,并返回它是否表示数字 42。该内建函数通过将其安装在 Math 对象上(因为我们可以)来暴露给 JS。

此示例演示了

如果您想在本地进行操作,以下代码基于修订版 589af9f2

定义 MathIs42 #

Torque 代码位于 src/builtins/*.tq 文件中,大致按主题组织。由于我们将编写一个 Math 内建函数,因此我们将把我们的定义放在 src/builtins/math.tq 中。由于此文件尚不存在,因此我们必须将其添加到 torque_files 中的 BUILD.gn 中。

namespace math {
javascript builtin MathIs42(
context: Context, receiver: Object, x: Object): Boolean {
// At this point, x can be basically anything - a Smi, a HeapNumber,
// undefined, or any other arbitrary JS object. ToNumber_Inline is defined
// in CodeStubAssembler. It inlines a fast-path (if the argument is a number
// already) and calls the ToNumber builtin otherwise.
const number: Number = ToNumber_Inline(x);
// A typeswitch allows us to switch on the dynamic type of a value. The type
// system knows that a Number can only be a Smi or a HeapNumber, so this
// switch is exhaustive.
typeswitch (number) {
case (smi: Smi): {
// The result of smi == 42 is not a Javascript boolean, so we use a
// conditional to create a Javascript boolean value.
return smi == 42 ? True : False;
}
case (heapNumber: HeapNumber): {
return Convert<float64>(heapNumber) == 42 ? True : False;
}
}
}
}

我们将定义放在 Torque 命名空间 math 中。由于此命名空间之前不存在,因此我们必须将其添加到 torque_namespaces 中的 BUILD.gn 中。

附加 Math.is42 #

Math 等内建对象主要在 src/bootstrapper.cc 中设置(一些设置发生在 .js 文件中)。附加我们的新内建函数很简单

// Existing code to set up Math, included here for clarity.
Handle<JSObject> math = factory->NewJSObject(cons, TENURED);
JSObject::AddProperty(global, name, math, DONT_ENUM);
// […snip…]
SimpleInstallFunction(isolate_, math, "is42", Builtins::kMathIs42, 1, true);

现在 is42 已附加,可以从 JS 中调用它

$ out/debug/d8
d8> Math.is42(42);
true
d8> Math.is42('42.0');
true
d8> Math.is42(true);
false
d8> Math.is42({ valueOf: () => 42 });
true

定义和调用具有桩链接的内建函数 #

内建函数也可以使用桩链接(而不是我们上面在 MathIs42 中使用的 JS 链接)创建。此类内建函数对于将常用代码提取到单独的代码对象中很有用,该代码对象可以被多个调用者使用,而代码只生成一次。让我们将处理堆数字的代码提取到一个名为 HeapNumberIs42 的单独内建函数中,并从 MathIs42 中调用它。

定义也很简单。与具有 Javascript 链接的内建函数唯一的区别是我们省略了关键字 javascript,并且没有接收者参数。

namespace math {
builtin HeapNumberIs42(implicit context: Context)(heapNumber: HeapNumber):
Boolean {

return Convert<float64>(heapNumber) == 42 ? True : False;
}

javascript builtin MathIs42(implicit context: Context)(
receiver: Object, x: Object): Boolean {
const number: Number = ToNumber_Inline(x);
typeswitch (number) {
case (smi: Smi): {
return smi == 42 ? True : False;
}
case (heapNumber: HeapNumber): {
// Instead of handling heap numbers inline, we now call our new builtin.
return HeapNumberIs42(heapNumber);
}
}
}
}

为什么你应该关心内建函数?为什么不将代码内联(或提取到宏中以提高可读性)?

一个重要的原因是代码空间:内建函数在编译时生成,并包含在 V8 快照中或嵌入到二进制文件中。将大量常用代码提取到单独的内建函数中可以快速节省 10 到 100 KB 的空间。

测试桩链接内建函数 #

即使我们的新内建函数使用非标准(至少是非 C++)调用约定,也可以为它编写测试用例。以下代码可以添加到 test/cctest/compiler/test-run-stubs.cc 中,以在所有平台上测试内建函数

TEST(MathIsHeapNumber42) {
HandleAndZoneScope scope;
Isolate* isolate = scope.main_isolate();
Heap* heap = isolate->heap();
Zone* zone = scope.main_zone();

StubTester tester(isolate, zone, Builtins::kMathIs42);
Handle<Object> result1 = tester.Call(Handle<Smi>(Smi::FromInt(0), isolate));
CHECK(result1->BooleanValue());
}