WebAssembly JavaScript builtins
WebAssembly JavaScript builtins sind Wasm-Äquivalente von JavaScript-Operationen, die eine Möglichkeit bieten, JavaScript-Funktionen innerhalb von Wasm-Modulen zu nutzen, ohne JavaScript-Zusatzcode importieren zu müssen, um eine Brücke zwischen JavaScript- und WebAssembly-Werten sowie Aufrufkonventionen bereitzustellen.
Dieser Artikel erklärt, wie builtins funktionieren und welche verfügbar sind, und bietet dann ein Verwendungsbeispiel.
Probleme beim Importieren von JavaScript-Funktionen
Für viele JavaScript-Funktionen funktionieren reguläre Importe gut. Das Importieren von Zusatzcode für Primitive wie String
, ArrayBuffer
und Map
bringt jedoch erhebliche Leistungseinbußen mit sich. In solchen Fällen erwarten WebAssembly und die meisten darauf zielenden Sprachen eine enge Abfolge von Inline-Operationen anstelle eines indirekten Funktionsaufrufs, wie es bei regulär importierten Funktionen der Fall ist.
Konkret führen Importe von Funktionen aus JavaScript in WebAssembly-Module zu Leistungsproblemen aus folgenden Gründen:
- Bestehende APIs erfordern eine Konvertierung zur Behandlung von Unterschieden im Wert
this
, den WebAssembly-Funktionsimporte alsundefined
belassen. - Bestimmte Primitive verwenden JavaScript-Operatoren wie
===
und<
, die nicht importiert werden können. - Die meisten JavaScript-Funktionen sind extrem großzügig bei den Arten von Werten, die sie akzeptieren. Es ist wünschenswert, das Typsystem von WebAssembly zu nutzen, um diese Überprüfungen und Umwandlungen wo immer möglich zu entfernen.
Angesichts dieser Probleme ist die Erstellung eingebauter Definitionen, die vorhandene JavaScript-Funktionen wie String
-Primitive an WebAssembly anpassen, einfacher und leistungsfähiger als deren Import und das Verlassen auf indirekte Funktionsaufrufe.
Verfügbare WebAssembly-JavaScript-builtins
Die folgenden Abschnitte erläutern die verfügbaren builtins. Weitere builtins werden wahrscheinlich in Zukunft unterstützt.
String-Operationen
Die verfügbaren String
-builtins sind:
"wasm:js-string" "cast"
-
Wirft einen Fehler, wenn der bereitgestellte Wert kein String ist. Grob äquivalent zu:
jsif (typeof obj !== "string") throw new WebAssembly.RuntimeError();
"wasm:js-string" "compare"
-
Vergleicht zwei Stringwerte und bestimmt deren Reihenfolge. Gibt
-1
zurück, wenn der erste String kleiner als der zweite ist,1
, wenn der erste String größer als der zweite ist, und0
, wenn die Strings streng gleich sind. "wasm:js-string" "concat"
-
Entspricht
String.prototype.concat()
. "wasm:js-string" "charCodeAt"
-
Entspricht
String.prototype.charCodeAt()
. "wasm:js-string" "codePointAt"
-
Entspricht
String.prototype.codePointAt()
. "wasm:js-string" "equals"
-
Vergleicht zwei Stringwerte auf strikte Gleichheit und gibt
1
zurück, wenn sie gleich sind, und0
, wenn nicht.Hinweis: Die Funktion
"equals"
ist die einzige String-builtin, die beinull
-Eingaben keinen Fehler wirft, sodass Wasm-Module keinenull
-Werte überprüfen müssen, bevor sie aufgerufen werden. Alle anderen Funktionen können mitnull
-Eingaben nicht vernünftig umgehen, deshalb werfen sie Fehler dafür. "wasm:js-string" "fromCharCode"
-
Entspricht
String.fromCharCode()
. "wasm:js-string" "fromCharCodeArray"
-
Erstellt einen String aus einem Wasm-Array von
i16
-Werten. "wasm:js-string" "fromCodePoint"
-
Entspricht
String.fromCodePoint()
. "wasm:js-string" "intoCharCodeArray"
-
Schreibt die Zeichencodes eines Strings in ein Wasm-Array von
i16
-Werten. "wasm:js-string" "length"
-
Entspricht
String.prototype.length
. "wasm:js-string" "substring"
-
Entspricht
String.prototype.substring()
. "wasm:js-string" "test"
-
Gibt
0
zurück, wenn der bereitgestellte Wert kein String ist, oder1
, wenn es ein String ist. Grob äquivalent zu:jstypeof obj === "string";
Wie verwendet man builtins?
Builtins funktionieren ähnlich wie aus JavaScript importierte Funktionen, mit dem Unterschied, dass Sie standardmäßige Wasm-Funktionsäquivalente zur Durchführung von JavaScript-Operationen nutzen, die in einem reservierten Namensraum (wasm:
) definiert sind. In diesem Fall können Browser optimalen Code für sie vorhersagen und generieren. Dieser Abschnitt fasst zusammen, wie man sie benutzt.
JavaScript-API
Builtins werden zur Kompilierungszeit aktiviert, indem die Eigenschaft compileOptions.builtins
als Argument beim Aufruf von Methoden zur Kompilierung und/oder Instanziierung eines Moduls angegeben wird. Ihr Wert ist ein Array von Strings, das die Sätze von builtins identifiziert, die Sie aktivieren möchten:
WebAssembly.compile(bytes, { builtins: ["js-string"] });
Das compileOptions
-Objekt steht den folgenden Funktionen zur Verfügung:
WebAssembly-Modul-Funktionen
In Ihrem WebAssembly-Modul können Sie nun builtins importieren, wie im compileOptions
-Objekt aus dem wasm:
-Namensraum angegeben (in diesem Fall die concat()
-Funktion; siehe auch die äquivalente eingebaute Definition):
(func $concat (import "wasm:js-string" "concat")
(param externref externref) (result (ref extern)))
Feature-Detection von builtins
Beim Verwenden von builtins sind Typprüfungen strikter als ohne sie — es werden bestimmte Regeln auf die builtin-Importe angewendet.
Um Code zur Feature-Detection für builtins zu schreiben, können Sie ein Modul definieren, das ungültig ist, wenn das Feature vorhanden ist, und gültig, wenn nicht. Sie geben dann true
zurück, wenn die Validierung fehlschlägt, um die Unterstützung anzuzeigen. Ein einfaches Modul, das dies erreicht, sieht folgendermaßen aus:
(module
(function (import "wasm:js-string" "cast")))
Ohne builtins ist das Modul gültig, da Sie jede Funktion mit beliebiger Signatur importieren können (in diesem Fall: keine Parameter und keine Rückgabewerte). Mit builtins ist das Modul ungültig, da die inzwischen spezialisierten Funktionen "wasm:js-string" "cast"
eine spezifische Signatur haben müssen (ein externref
-Parameter und ein nicht-nullfähiger (ref extern)
Rückgabewert).
Sie können dann versuchen, dieses Modul mit der validate()
-Methode zu validieren, aber beachten Sie, wie das Ergebnis mit dem !
-Operator negiert wird — denken Sie daran, dass builtins unterstützt werden, wenn das Modul ungültig ist:
const compileOptions = {
builtins: ["js-string"],
};
fetch("module.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.validate(bytes, compileOptions))
.then((result) => console.log(`Builtins available: ${!result}`));
Der obige Modulkode ist so kurz, dass Sie einfach die literalen Bytes validieren könnten, anstatt das Modul herunterzuladen. Eine Feature-Detection-Funktion könnte so aussehen:
function JsStringBuiltinsSupported() {
let bytes = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 23, 1, 14, 119, 97, 115,
109, 58, 106, 115, 45, 115, 116, 114, 105, 110, 103, 4, 99, 97, 115, 116, 0,
0,
]);
return !WebAssembly.validate(bytes, { builtins: ["js-string"] });
}
Hinweis: In vielen Fällen gibt es Alternativen zur Feature-Detection von builtins. Eine andere Option könnte sein, reguläre Importe zusammen mit den builtins bereitzustellen, wobei unterstützende Browser die Fallbacks einfach ignorieren.
Builtins-Beispiel
Lassen Sie uns ein einfaches, aber vollständiges Beispiel durchgehen, um zu zeigen, wie builtins verwendet werden. Dieses Beispiel definiert eine Funktion innerhalb eines Wasm-Moduls, die zwei Strings zusammenfügt und das Ergebnis in der Konsole ausgibt, dann exportiert sie es. Wir werden dann die exportierte Funktion aus JavaScript aufrufen.
Das Beispiel, auf das wir Bezug nehmen werden, verwendet die Funktion WebAssembly.instantiate()
auf der Webseite, um die Kompilierung und Instantiierung zu handhaben; Sie können dieses und andere Beispiele in unserem webassembly-examples
Repository finden — siehe js-builtin-examples
.
Sie können das Beispiel durch die folgenden Schritte aufbauen. Zusätzlich können Sie es live sehen — öffnen Sie die JavaScript-Konsole Ihres Browsers, um die Ausgabebeispiele zu sehen.
JavaScript
Das JavaScript für das Beispiel wird unten gezeigt. Um dies lokal zu testen, fügen Sie es in eine HTML-Seite mit einer Methode Ihrer Wahl ein (zum Beispiel innerhalb von <script>
-Tags oder in einer externen .js
-Datei, die über <script src="">
referenziert wird).
const importObject = {
// Regular import
m: {
log: console.log,
},
};
const compileOptions = {
builtins: ["js-string"], // Enable JavaScript string builtins
importedStringConstants: "string_constants", // Enable imported global string constants
};
fetch("log-concat.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject, compileOptions))
.then((result) => result.instance.exports.main());
Das JavaScript:
- Definiert ein
importObject
, das eine Funktion"log"
in einem Namensraum"m"
spezifiziert, die während der Instanziierung in das Wasm-Modul importiert wird. Es handelt sich um dieconsole.log()
Funktion. - Definiert ein
compileOptions
-Objekt, das umfasst:- die
builtins
Eigenschaft, um String-builtins zu aktivieren. - die
importedStringConstants
Eigenschaft, um importierte globale String-Konstanten zu aktivieren.
- die
- Verwendet
fetch()
, um das Wasm-Modul (log-concat.wasm
) zu laden, konvertiert die Antwort in einArrayBuffer
mitResponse.arrayBuffer
und kompiliert und instanziiert dann das Wasm-Modul mitWebAssembly.instantiate()
. - Ruft die
main()
-Funktion auf, die aus dem Wasm-Modul exportiert wird.
Wasm-Modul
Die Textdarstellung unseres WebAssembly-Modulkodes sieht so aus:
(module
(global $h (import "string_constants" "hello ") externref)
(global $w (import "string_constants" "world!") externref)
(func $concat (import "wasm:js-string" "concat")
(param externref externref) (result (ref extern)))
(func $log (import "m" "log") (param externref))
(func (export "main")
(call $log (call $concat (global.get $h) (global.get $w))))
)
Dieser Code:
- Importiert zwei globale String-Konstanten,
"hello "
und"world!"
, mit dem Namensraum"string_constants"
, wie im JavaScript angegeben. Sie erhalten die Namen$h
und$w
. - Importiert das
concat
builtin aus demwasm:
-Namensraum, gibt ihm den Namen$concat
und spezifiziert, dass es zwei Parameter und einen Rückgabewert hat. - Importiert die importierte
"log"
-Funktion aus dem"m"
-Namensraum, wie im JavaScriptimportObject
-Objekt angegeben, gibt ihr den Namen$log
und spezifiziert, dass sie einen Parameter hat. Wir haben beschlossen, in diesem Beispiel neben einem builtin auch einen regulären Import zu verwenden, um Ihnen zu zeigen, wie die beiden Ansätze vergleichbar sind. - Definiert eine Funktion, die mit dem Namen
"main"
exportiert wird. Diese Funktion ruft$log
auf und übergibt einen$concat
-Aufruf als Parameter. Der$concat
-Aufruf wird mit den$h
und$w
globalen String-Konstanten als Parameter aufgerufen.
Um Ihr lokales Beispiel zum Laufen zu bringen:
-
Speichern Sie den oben gezeigten WebAssembly-Modulcode in einer Textdatei mit dem Namen
log-concat.wat
im selben Verzeichnis wie Ihr HTML/JavaScript. -
Kompilieren Sie es mit dem
wasm-as
-Tool in ein WebAssembly-Modul (log-concat.wasm
), das Teil der Binaryen-Bibliothek ist (siehe die Build-Anleitung). Sie müssenwasm-as
mit aktivierten Referenztypen und Garbage Collection (GC) für diese Beispiele ausführen, damit sie erfolgreich kompiliert werden:shwasm-as --enable-reference-types -–enable-gc log-concat.wat
Oder Sie können das
-all
-Flag anstelle von--enable-reference-types -–enable-gc
verwenden:shwasm-as -all log-concat.wat
-
Laden Sie Ihre Beispiel-HTML-Seite in einem unterstützenden Browser mithilfe eines lokalen HTTP-Servers.
Das Ergebnis sollte eine leere Webseite sein, mit "hello world!"
, das in die JavaScript-Konsole geloggt wird und von einer exportierten Wasm-Funktion erzeugt wird. Das Logging wurde mit einer aus JavaScript importierten Funktion durchgeführt, während das Zusammenfügen der beiden ursprünglichen Strings durch ein builtin erfolgte.