V8 Torque builtins
Этот документ предназначен для введения в написание встроенных функций Torque и ориентирован на разработчиков V8. Torque заменяет CodeStubAssembler в качестве рекомендуемого способа реализации новых встроенных функций. См. CodeStubAssembler встроенные функции для версии этого руководства, основанной на CSA.
Встроенные функции
В V8 встроенные функции можно рассматривать как фрагменты кода, которые могут выполняться виртуальной машиной во время выполнения. Частый случай использования - это реализация функций встроенных объектов (таких как RegExp
или Promise
), но встроенные функции также могут использоваться для предоставления другой внутренней функциональности (например, в рамках системы IC).
Встроенные функции V8 можно реализовать различными методами (каждый из которых имеет свои преимущества и недостатки):
- Зависимый от платформы ассемблерный язык: может быть очень эффективным, но требует ручной портировки для всех платформ и труден в обслуживании.
- C++: очень похож на стиль функций времени выполнения и имеет доступ к мощным функциям времени выполнения V8, но обычно не подходит для областей, чувствительных к производительности.
- JavaScript: лаконичный и читаемый код, доступ к быстрым интринсикам, но частое использование медленных вызовов времени выполнения, подверженность непредсказуемой производительности из-за загрязнения типов и скрытые проблемы, связанные с (сложной и неочевидной) семантикой JS. Использование встроенных функций на Javascript устарело, и их добавление не рекомендуется.
- CodeStubAssembler: предоставляет эффективную низкоуровневую функциональность, очень близкую к ассемблерному языку, при этом оставаясь независимой от платформы и сохраняя читаемость.
- V8 Torque: это специфичный для V8 язык предметной области, который переводится в CodeStubAssembler. Таким образом, он расширяет возможности CodeStubAssembler и предлагает статическую типизацию, а также читаемый и выразительный синтаксис.
Оставшаяся часть документа сосредоточена на последнем случае и дает краткий урок по разработке простой встроенной функции Torque, доступной в JavaScript. Для получения более полной информации о Torque см. Руководство пользователя V8 Torque.
Написание встроенной функции Torque
В этом разделе мы напишем простую встроенную функцию CSA, которая принимает один аргумент и возвращает, соответствует ли он числу 42
. Эта встроенная функция будет доступна в JS, установив её на объекте Math
(потому что мы можем).
Этот пример демонстрирует:
- Создание встроенной функции Torque с JavaScript-связью, которая может вызываться как функция JS.
- Использование Torque для реализации простой логики: различия типов, обработки Smi и heap-number, условных конструкций.
- Установку встроенной функции CSA на объект
Math
.
Если вы хотите следовать вместе с этим руководством локально, приведенный ниже код основан на ревизии 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 {
// На данном этапе, x может быть практически чем угодно - Smi, HeapNumber,
// undefined или любым другим произвольным JS объектом. ToNumber_Inline определяется
// в CodeStubAssembler. Он встраивает более быстрый путь (если аргумент уже число)
// и вызывает встроенную функцию ToNumber в противном случае.
const number: Number = ToNumber_Inline(x);
// Переключение типов позволяет нам переключаться на динамический тип значения. Система
// типов знает, что Number может быть только Smi или HeapNumber, поэтому это
// переключение является исчерпывающим.
typeswitch (number) {
case (smi: Smi): {
// Результат smi == 42 не является логическим значением Javascript, поэтому мы используем
// условие для создания логического значения Javascript.
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
файлах). Присоединить наш новый встроенный объект достаточно просто:
// Существующий код для настройки Math, приведен здесь для ясности.
Handle<JSObject> math = factory->NewJSObject(cons, TENURED);
JSObject::AddProperty(global, name, math, DONT_ENUM);
// […упущено…]
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
Определение и вызов встроенной функции с использованием стуб-связывания
Встроенные функции также могут быть созданы с использованием стуб-связывания (вместо JS-связывания, как в случае с MathIs42
). Такие функции могут быть полезны для выделения часто используемого кода в отдельный объект кода, который может использоваться несколькими вызывающими объектами, при этом код создается только один раз. Давайте выделим код, который обрабатывает числа с плавающей запятой, в отдельную встроенную функцию под названием 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): {
// Вместо обработки чисел с плавающей запятой линейно, мы теперь вызываем нашу новую встроенную функцию.
return HeapNumberIs42(heapNumber);
}
}
}
}
Почему вам стоит заботиться о встроенных функциях? Почему бы не оставить код линейным (или извлечённым в макросы для лучшей читаемости)?
Важной причиной является экономия места в коде: встроенные функции создаются во время компиляции и включаются в слепок V8 или встраиваются в бинарный файл. Выделение больших фрагментов часто используемого кода в отдельные встроенные функции может быстро привести к экономии места в десятки и сотни килобайт.
Тестирование встроенных функций с использованием стуб-связывания
Хотя наша новая встроенная функция использует нестандартный (по крайней мере, не 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());
}