Hier erfahren Sie, wie Sie JavaScript-Code in Ihre WebAssembly-Bibliothek einbetten, um mit der Außenwelt zu kommunizieren.
Wenn Sie WebAssembly in das Web einbinden, benötigen Sie eine Möglichkeit, externe APIs wie Web-APIs und Drittanbieterbibliotheken aufzurufen. Sie benötigen dann eine Möglichkeit, die von diesen APIs zurückgegebenen Werte und Objektinstanzen zu speichern und diese gespeicherten Werte später an andere APIs weiterzugeben. Bei asynchronen APIs müssen Sie möglicherweise auch mit Asyncify in Ihrem synchronen C/C++-Code auf Versprechen warten und das Ergebnis lesen, sobald der Vorgang abgeschlossen ist.
Emscripten bietet mehrere Tools für solche Interaktionen:
emscripten::val
zum Speichern und Bearbeiten von JavaScript-Werten in C++.EM_JS
zum Einbetten von JavaScript-Snippets und zum Binden als C/C++-Funktionen.EM_ASYNC_JS
, dasEM_JS
ähnelt, aber das Einbetten asynchroner JavaScript-Snippets vereinfacht.EM_ASM
zum Einbetten kurzer Snippets und deren Inline-Ausführung, ohne eine Funktion zu deklarieren.--js-library
für erweiterte Szenarien, in denen Sie viele JavaScript-Funktionen gemeinsam als einzelne Bibliothek deklarieren möchten.
In diesem Beitrag erfahren Sie, wie Sie sie alle für ähnliche Aufgaben verwenden.
emscripten::val-Klasse
Die Klasse emcripten::val
wird von Embind bereitgestellt. Es kann globale APIs aufrufen, JavaScript-Werte an C++-Instanzen binden und Werte zwischen C++- und JavaScript-Typen konvertieren.
So kannst du es mit Asyncifys .await()
verwenden, um JSON abzurufen und zu parsen:
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
Dieser Code funktioniert gut, führt aber viele Zwischenschritte aus. Für jeden Vorgang auf val
müssen die folgenden Schritte ausgeführt werden:
- C++-Werte, die als Argumente übergeben werden, in ein Zwischenformat konvertieren.
- Rufen Sie JavaScript auf, lesen Sie die Argumente und konvertieren Sie sie in JavaScript-Werte.
- Funktion ausführen
- Wandeln Sie das Ergebnis aus JavaScript in ein Zwischenformat um.
- Das umgewandelte Ergebnis wird an C++ zurückgegeben und dort schließlich gelesen.
Jede await()
muss außerdem die C++-Seite pausieren, indem der gesamte Aufrufstapel des WebAssembly-Moduls zurückgewickelt, zu JavaScript zurückgekehrt, gewartet und der WebAssembly-Stapel wiederhergestellt wird, sobald der Vorgang abgeschlossen ist.
Für diesen Code ist kein C++-Code erforderlich. C++-Code dient nur als Treiber für eine Reihe von JavaScript-Vorgängen. Was wäre, wenn Sie fetch_json
in JavaScript umwandeln und gleichzeitig den Overhead der Zwischenschritte reduzieren könnten?
EM_JS-Makro
Mit EM_JS macro
können Sie fetch_json
in JavaScript verschieben. Mit EM_JS
in Emscripten können Sie eine C/C++-Funktion deklarieren, die durch ein JavaScript-Snippet implementiert wird.
Wie WebAssembly selbst unterstützt es nur numerische Argumente und Rückgabewerte. Wenn Sie andere Werte übergeben möchten, müssen Sie sie manuell über die entsprechenden APIs konvertieren. Hier einige Beispiele:
Für das Übergeben von Zahlen ist keine Konvertierung erforderlich:
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
Wenn du Strings an JavaScript übergeben und von JavaScript empfangen möchtest, musst du die entsprechenden Konvertierungs- und Zuweisungsfunktionen aus preamble.js verwenden:
EM_JS(void, log_string, (const char *msg), {
console.log(UTF8ToString(msg));
});
EM_JS(const char *, get_input, (), {
let str = document.getElementById('myinput').value;
// Returns heap-allocated string.
// C/C++ code is responsible for calling `free` once unused.
return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});
Für komplexere, beliebige Werttypen können Sie die JavaScript API für die oben erwähnte val
-Klasse verwenden. Damit können Sie JavaScript-Werte und C++-Klassen in Zwischen-Handles und zurück konvertieren:
EM_JS(void, log_value, (EM_VAL val_handle), {
let value = Emval.toValue(val_handle);
console.log(value);
});
EM_JS(EM_VAL, find_myinput, (), {
let input = document.getElementById('myinput');
return Emval.toHandle(input);
});
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }
val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();
Mit diesen APIs im Hinterkopf könnte das fetch_json
-Beispiel so umgeschrieben werden, dass die meisten Aufgaben ohne Verlassen von JavaScript erledigt werden:
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
Es gibt noch einige explizite Konvertierungen am Anfang und Ende der Funktion, aber der Rest ist jetzt regulärer JavaScript-Code. Im Gegensatz zum val
-Äquivalent kann es jetzt von der JavaScript-Engine optimiert werden und die C++-Seite muss nur einmal für alle asynchronen Vorgänge angehalten werden.
EM_ASYNC_JS-Makro
Das einzige Element, das nicht schön aussieht, ist der Asyncify.handleAsync
-Wrapper. Sein einziger Zweck besteht darin, die Ausführung von async
-JavaScript-Funktionen mit Asyncify zu ermöglichen. Dieser Anwendungsfall ist so häufig, dass es jetzt ein spezielles EM_ASYNC_JS
-Makro gibt, das beide Funktionen kombiniert.
So könnten Sie damit die endgültige Version des fetch
-Beispiels erstellen:
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
EM_ASM
EM_JS
wird empfohlen, um JavaScript-Snippets zu deklarieren. Es ist effizient, da die deklarierten Snippets wie alle anderen JavaScript-Funktionsimporte direkt gebunden werden. Außerdem können Sie alle Parametertypen und ‑namen explizit deklarieren, was die Ergonomie verbessert.
In einigen Fällen möchten Sie jedoch ein kurzes Snippet für einen console.log
-Aufruf, eine debugger;
-Anweisung oder etwas Ähnliches einfügen und sich nicht mit der Deklarierung einer ganzen separaten Funktion befassen. In diesen seltenen Fällen ist ein EM_ASM macros family
(EM_ASM
, EM_ASM_INT
und EM_ASM_DOUBLE
) möglicherweise die einfachere Wahl. Diese Makros ähneln dem EM_JS
-Makro, führen aber Code an der Stelle aus, an der sie eingefügt werden, anstatt eine Funktion zu definieren.
Da sie keinen Funktionsprototyp deklarieren, müssen sie den Rückgabetyp und den Zugriff auf Argumente auf andere Weise angeben.
Sie müssen den richtigen Makronamen verwenden, um den Rückgabetyp auszuwählen. EM_ASM
-Blöcke sollten wie void
-Funktionen funktionieren, EM_ASM_INT
-Blöcke können einen Ganzzahlwert zurückgeben und EM_ASM_DOUBLE
-Blöcke geben entsprechend Gleitkommazahlen zurück.
Alle übergebenen Argumente sind im JavaScript-Body unter den Namen $0
, $1
usw. verfügbar. Wie bei EM_JS
oder WebAssembly im Allgemeinen sind die Argumente auf numerische Werte beschränkt: Ganzzahlen, Gleitkommazahlen, Zeiger und Handles.
Hier ist ein Beispiel dafür, wie Sie mit einem EM_ASM
-Makro einen beliebigen JS-Wert in der Konsole protokollieren können:
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
--js-library
Außerdem unterstützt Emscripten die Deklaration von JavaScript-Code in einer separaten Datei in einem benutzerdefinierten Bibliotheksformat:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Anschließend müssen Sie die entsprechenden Prototypen manuell auf der C++-Seite deklarieren:
extern "C" void log_value(EM_VAL val_handle);
Nachdem die JavaScript-Bibliothek auf beiden Seiten deklariert wurde, kann sie über --js-library option
mit dem Hauptcode verknüpft werden, um Prototypen mit entsprechenden JavaScript-Implementierungen zu verbinden.
Dieses Modulformat ist jedoch nicht standardmäßig und erfordert sorgfältige Abhängigkeitsanmerkungen. Daher ist sie hauptsächlich für erweiterte Szenarien vorgesehen.
Fazit
In diesem Beitrag haben wir uns verschiedene Möglichkeiten angesehen, wie Sie JavaScript-Code in C++ einbinden können, wenn Sie mit WebAssembly arbeiten.
Mithilfe solcher Snippets können Sie lange Abfolgen von Vorgängen übersichtlicher und effizienter ausdrücken und Drittanbieterbibliotheken, neue JavaScript APIs und sogar JavaScript-Syntaxfunktionen nutzen, die noch nicht über C++ oder Embind ausgedrückt werden können.