V8 Torque Builtins
Dieses Dokument soll eine Einführung in das Schreiben von Torque-Builtins geben und richtet sich an V8-Entwickler. Torque ersetzt CodeStubAssembler als die empfohlene Methode zur Implementierung neuer Builtins. Siehe CodeStubAssembler Builtins für die CSA-Version dieses Leitfadens.
Builtins
In V8 können Builtins als Codeabschnitte betrachtet werden, die zur Laufzeit vom VM ausführbar sind. Ein häufiges Anwendungsbeispiel ist die Implementierung von Funktionen eingebauter Objekte (wie RegExp
oder Promise
), aber Builtins können auch verwendet werden, um andere interne Funktionalitäten bereitzustellen (z. B. als Teil des IC-Systems).
Die Builtins von V8 können mit verschiedenen Methoden implementiert werden (jede mit unterschiedlichen Abwägungen):
- Plattformabhängige Assemblersprache: kann sehr effizient sein, erfordert jedoch manuelle Ports auf alle Plattformen und ist schwierig zu warten.
- C++: sehr ähnlich im Stil zu Laufzeitfunktionen und hat Zugriff auf V8s leistungsstarke Laufzeitfunktionalität, ist jedoch in der Regel nicht für leistungsintensive Bereiche geeignet.
- JavaScript: prägnanter und lesbarer Code, Zugang zu schnellen Intrinsics, häufige Nutzung langsamer Laufzeitaufrufe, anfällig für unvorhersehbare Leistung durch Typverschmutzung und subtile Probleme rund um (komplizierte und nicht offensichtliche) JS-Semantik. JavaScript Builtins sind veraltet und sollten nicht mehr hinzugefügt werden.
- CodeStubAssembler: bietet effiziente Low-Level-Funktionalität, die der Assemblersprache sehr nahe kommt, bleibt dabei plattformunabhängig und erhält die Lesbarkeit.
- V8 Torque: ist eine spezifische Domänensprache für V8, die in CodeStubAssembler übersetzt wird. Somit erweitert es CodeStubAssembler und bietet statische Typisierung sowie eine lesbare und ausdrucksstarke Syntax.
Das verbleibende Dokument konzentriert sich auf letzteres und bietet ein kurzes Tutorial zur Entwicklung eines einfachen Torque-Builtins, das in JavaScript verfügbar ist. Für umfassendere Informationen zu Torque siehe das V8 Torque Benutzerhandbuch.
Schreiben eines Torque Builtins
In diesem Abschnitt werden wir ein einfaches CSA-Builtin schreiben, das ein einzelnes Argument entgegennimmt und zurückgibt, ob es die Zahl 42
darstellt. Das Builtin wird in JS verfügbar gemacht, indem es auf dem Math
-Objekt installiert wird (weil wir es können).
Dieses Beispiel demonstriert:
- Erstellung eines Torque-Builtins mit JavaScript-Verknüpfung, das wie eine JS-Funktion aufgerufen werden kann.
- Nutzung von Torque zur Implementierung einfacher Logik: Typunterscheidung, Umgang mit Smi und Heap-Nummern, Bedingungsanweisungen.
- Installation des CSA-Builtins auf dem
Math
-Objekt.
Falls Sie lokal folgen möchten, basiert der folgende Code auf der Revision 589af9f2.
Definition von MathIs42
Torque-Code befindet sich in Dateien src/builtins/*.tq
, die grob nach Themen organisiert sind. Da wir ein Math
-Builtin schreiben werden, fügen wir unsere Definition in src/builtins/math.tq
ein. Da diese Datei noch nicht existiert, müssen wir sie zu torque_files
in BUILD.gn
hinzufügen.
namespace math {
javascript builtin MathIs42(
context: Context, receiver: Object, x: Object): Boolean {
// An dieser Stelle kann x im Grunde alles sein - ein Smi, eine HeapNumber,
// undefined oder irgendein anderes beliebiges JS-Objekt. ToNumber_Inline wird
// im CodeStubAssembler definiert. Es integriert einen schnellen Pfad (falls das
// Argument bereits eine Zahl ist) und ruft andernfalls das ToNumber-Builtin auf.
const number: Number = ToNumber_Inline(x);
// Ein typeswitch erlaubt es uns, basierend auf dem dynamischen Typ eines Wertes zu wechseln. Das Typsystem
// weiß, dass eine Zahl nur ein Smi oder eine HeapNumber sein kann, daher ist dieser
// Switch erschöpfend.
typeswitch (number) {
case (smi: Smi): {
// Das Ergebnis von smi == 42 ist kein Javascript-Boolescher Wert, daher verwenden wir eine
// Bedingung, um einen Javascript-Booleschen Wert zu erstellen.
return smi == 42 ? True : False;
}
case (heapNumber: HeapNumber): {
return Convert<float64>(heapNumber) == 42 ? True : False;
}
}
}
}
Wir setzen die Definition in den Torque-Namensraum math
. Da dieser Namensraum zuvor nicht existierte, müssen wir ihn zu torque_namespaces
in BUILD.gn
hinzufügen.
Anhängen von Math.is42
Eingebaute Objekte wie Math
werden größtenteils in src/bootstrapper.cc
eingerichtet (mit einigen Setups, die in .js
-Dateien erfolgen). Das Hinzufügen unseres neuen eingebauten Elements ist einfach:
// Vorhandener Code zum Einrichten von Math, hier zur Verdeutlichung enthalten.
Handle<JSObject> math = factory->NewJSObject(cons, TENURED);
JSObject::AddProperty(global, name, math, DONT_ENUM);
// […snip…]
SimpleInstallFunction(isolate_, math, "is42", Builtins::kMathIs42, 1, true);
Jetzt, da is42
angehängt ist, kann es aus JS aufgerufen werden:
$ 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
Definition und Aufruf eines eingebauten Elements mit Stub-Verknüpfung
Eingebaute Elemente können auch mit Stub-Verknüpfung erstellt werden (anstatt der JS-Verknüpfung, die wir oben bei MathIs42
verwendet haben). Solche eingebauten Elemente können nützlich sein, um häufig verwendeten Code in ein separates Codeobjekt auszulagern, das von mehreren Aufrufern genutzt werden kann, während der Code nur einmal erstellt wird. Lassen Sie uns den Code, der Heap-Zahlen behandelt, in ein separates eingebautes Element namens HeapNumberIs42
auslagern und es von MathIs42
aufrufen.
Die Definition ist ebenfalls unkompliziert. Der einzige Unterschied zu unserem eingebauten Element mit JavaScript-Verknüpfung besteht darin, dass wir das Schlüsselwort javascript
weglassen und kein Empfängerargument vorhanden ist.
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): {
// Anstatt Heap-Zahlen inline zu behandeln, rufen wir jetzt unser neues eingebautes Element auf.
return HeapNumberIs42(heapNumber);
}
}
}
}
Warum sollten Sie sich überhaupt für eingebaute Elemente interessieren? Warum nicht den Code inline belassen (oder in Makros für bessere Lesbarkeit auslagern)?
Ein wichtiger Grund ist der Speicherplatz: Eingebaute Elemente werden zur Kompilierungszeit erstellt und in den V8-Snapshot aufgenommen oder in die Binärdatei eingebettet. Das Auslagern großer Codeblöcke, die häufig verwendet werden, in separate eingelassene Elemente kann schnell zu Platzersparnissen von 10 bis 100 KB führen.
Testen eingebauter Elemente mit Stub-Verknüpfung
Auch wenn unser neues eingebautes Element eine nicht standardmäßige (zumindest keine C++) Aufrufkonvention verwendet, ist es möglich, Testfälle dafür zu schreiben. Der folgende Code kann test/cctest/compiler/test-run-stubs.cc
hinzugefügt werden, um das eingebaute Element auf allen Plattformen zu testen:
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());
}