Es bindet JS an Ihr WASM.
In meinem letzten WASM-Artikel habe ich beschrieben, wie man eine C-Bibliothek in WASM kompiliert, damit sie im Web verwendet werden kann. Eine Sache, die mir (und vielen Lesern) aufgefallen ist, ist die umständliche und etwas unbeholfene Art und Weise, wie Sie manuell deklarieren müssen, welche Funktionen Ihres WASM-Moduls Sie verwenden. Zur Erinnerung: Das ist das Code-Snippet, um das es geht:
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
Hier deklarieren wir die Namen der Funktionen, die wir mit EMSCRIPTEN_KEEPALIVE markiert haben, sowie ihre Rückgabetypen und die Typen ihrer Argumente. Anschließend können wir die Methoden für das api-Objekt verwenden, um diese Funktionen aufzurufen. Bei dieser Verwendung von WASM werden jedoch keine Strings unterstützt und Sie müssen Speicherblöcke manuell verschieben, was die Verwendung vieler Bibliotheks-APIs sehr mühsam macht. Gibt es keine bessere Lösung? Ja, sonst gäbe es diesen Artikel nicht.
C++-Namensmangling
Die Entwicklerfreundlichkeit wäre Grund genug, ein Tool zu entwickeln, das bei diesen Bindungen hilft. Es gibt jedoch einen dringenderen Grund: Wenn Sie C- oder C++-Code kompilieren, wird jede Datei separat kompiliert. Anschließend werden alle diese sogenannten Objektdateien von einem Linker zusammengeführt und in eine WASM-Datei umgewandelt. In C sind die Namen der Funktionen weiterhin in der Objektdatei verfügbar, damit der Linker sie verwenden kann. Um eine C-Funktion aufrufen zu können, benötigen Sie nur den Namen, den wir als String an cwrap() übergeben.
C++ unterstützt dagegen die Überladung von Funktionen. Das bedeutet, dass Sie dieselbe Funktion mehrmals implementieren können, solange die Signatur unterschiedlich ist (z. B. Parameter mit unterschiedlichen Typen). Auf Compilerebene wird ein Name wie add in etwas umgewandelt, das die Signatur im Funktionsnamen für den Linker codiert. Daher können wir unsere Funktion nicht mehr über ihren Namen aufrufen.
embind eingeben
embind ist Teil der Emscripten-Toolchain und bietet eine Reihe von C++-Makros, mit denen Sie C++-Code annotieren können. Sie können deklarieren, welche Funktionen, Enums, Klassen oder Werttypen Sie in JavaScript verwenden möchten. Beginnen wir mit einigen einfachen Funktionen:
#include <emscripten/bind.h>
using namespace emscripten;
double add(double a, double b) {
return a + b;
}
std::string exclaim(std::string message) {
return message + "!";
}
EMSCRIPTEN_BINDINGS(my_module) {
function("add", &add);
function("exclaim", &exclaim);
}
Im Vergleich zu meinem vorherigen Artikel wird emscripten.h nicht mehr verwendet, da wir unsere Funktionen nicht mehr mit EMSCRIPTEN_KEEPALIVE annotieren müssen.
Stattdessen haben wir einen EMSCRIPTEN_BINDINGS-Abschnitt, in dem wir die Namen auflisten, unter denen wir unsere Funktionen für JavaScript verfügbar machen möchten.
Zum Kompilieren dieser Datei können wir dieselbe Einrichtung (oder, wenn Sie möchten, dasselbe Docker-Image) wie im vorherigen Artikel verwenden. Um embind zu verwenden, fügen wir das Flag --bind hinzu:
$ emcc --bind -O3 add.cpp
Jetzt müssen wir nur noch eine HTML-Datei erstellen, mit der unser neu erstelltes WASM-Modul geladen wird:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
Wie Sie sehen, verwenden wir cwrap() nicht mehr. Das funktioniert sofort. Noch wichtiger ist jedoch, dass wir uns nicht mehr darum kümmern müssen, Speicherblöcke manuell zu kopieren, damit Strings funktionieren. embind bietet Ihnen das kostenlos, zusammen mit Typüberprüfungen:
Das ist sehr praktisch, da wir einige Fehler frühzeitig erkennen können, anstatt uns mit den manchmal recht unhandlichen WASM-Fehlern auseinandersetzen zu müssen.
Objekte
Viele JavaScript-Konstruktoren und -Funktionen verwenden Optionenobjekte. Das ist ein schönes Muster in JavaScript, aber es ist extrem mühsam, es manuell in WASM zu realisieren. embind kann auch hier helfen.
Ich habe zum Beispiel diese unglaublich nützliche C++-Funktion entwickelt, die meine Strings verarbeitet, und ich möchte sie dringend im Web verwenden. So bin ich vorgegangen:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
struct ProcessMessageOpts {
bool reverse;
bool exclaim;
int repeat;
};
std::string processMessage(std::string message, ProcessMessageOpts opts) {
std::string copy = std::string(message);
if(opts.reverse) {
std::reverse(copy.begin