V8 Torque 内建函数
本文档旨在作为编写 Torque 内建函数的入门指南,面向 V8 开发人员。Torque 替代 CodeStubAssembler 成为实现新内建函数的推荐方式。有关此指南的 CSA 版本,请参阅 CodeStubAssembler 内建函数。
内建函数 #
在 V8 中,内建函数可以看作是在运行时由 VM 执行的代码块。一个常见的用例是实现内建对象的函数(例如 RegExp
或 Promise
),但内建函数也可以用于提供其他内部功能(例如作为 IC 系统的一部分)。
V8 的内建函数可以使用多种不同的方法实现(每种方法都有不同的权衡)
- 平台相关的汇编语言:可以非常高效,但需要手动移植到所有平台,并且难以维护。
- C++:在风格上与运行时函数非常相似,并且可以访问 V8 的强大运行时功能,但通常不适合性能敏感的区域。
- JavaScript:简洁易读的代码,可以访问快速内联函数,但经常使用缓慢的运行时调用,容易受到类型污染导致的不可预测性能的影响,以及围绕(复杂且不明显的)JS 语义的微妙问题。Javascript 内建函数已弃用,不再添加。
- CodeStubAssembler:提供高效的低级功能,非常接近汇编语言,同时保持平台无关性并保留可读性。
- V8 Torque:是一种 V8 特定的领域特定语言,它被翻译成 CodeStubAssembler。因此,它扩展了 CodeStubAssembler,并提供静态类型以及可读且表达力强的语法。
本文档的其余部分将重点介绍后者,并简要介绍如何开发一个暴露给 JavaScript 的简单 Torque 内建函数。有关 Torque 的更完整信息,请参阅 V8 Torque 用户手册。
编写 Torque 内建函数 #
在本节中,我们将编写一个简单的 CSA 内建函数,它接受一个参数,并返回它是否表示数字 42
。该内建函数通过将其安装在 Math
对象上(因为我们可以)来暴露给 JS。
此示例演示了
- 使用 JavaScript 链接创建 Torque 内建函数,可以像 JS 函数一样调用它。
- 使用 Torque 实现简单的逻辑:类型区分、Smi 和堆数字处理、条件语句。
- 在
Math
对象上安装 CSA 内建函数。
如果您想在本地进行操作,以下代码基于修订版 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());
}