Es bindet JS an Ihr Wasm.
In meinem letzten Wasm-Artikel habe ich wie man eine C-Bibliothek in Wasm kompiliert, um sie im Web verwenden zu können. Eine Sache mir (und vielen Lesern) aufgefallen ist, ist die grobe und etwas peinliche Art, Sie müssen manuell deklarieren, welche Funktionen Ihres Wasm-Moduls Sie verwenden. Zur Erinnerung hier ist das Code-Snippet, von dem ich rede:
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
Hier legen wir die Namen der Funktionen fest, die wir mit
EMSCRIPTEN_KEEPALIVE
, welche Rückgabetypen sie haben und welche Typen sie
Argumente. Danach können wir die Methoden für das api
-Objekt verwenden, um
für diese Funktionen. Die Verwendung von Wasm
unterstützt jedoch keine Zeichenfolgen und
erfordert das manuelle Verschieben von Speicherblöcken. Dadurch werden viele
APIs sehr umständlich. Gibt es eine bessere Lösung? Warum ja, sonst
Worum geht es in diesem Artikel?
Ändern von C++-Namen
Die Entwicklererfahrung wäre zwar ausreichend, um ein Tool zu entwickeln,
für diese Bindungen gibt es einen dringenderen Grund: Wenn Sie C kompilieren,
oder C++ Code schreiben, wird jede Datei separat kompiliert. Eine Verknüpfung kümmert sich dann um
all diese sogenannten Objektdateien zusammengefügt und in einen Wasm verwandelt.
-Datei. Mit C sind die Namen der Funktionen weiterhin in der Objektdatei verfügbar.
für die Verknüpfung. Alles, was Sie brauchen,
um eine C-Funktion aufzurufen, ist der Name,
die als String für cwrap()
bereitgestellt wird.
C++ hingegen unterstützt
Funktionsüberlastung, d. h. Sie können
dieselbe Funktion mehrfach verwenden, solange sich die Signatur unterscheidet (z.B.
unterschiedlich typisierten Parametern). Auf Compiler-Ebene ein schöner Name wie add
würden verzerrt und so die Signatur in der Funktion codiert
Name für die Verknüpfung. Daher könnten wir unsere Funktion nicht
mit seinem Namen.
embind eingeben
embind ist Teil der Emscripten-Toolchain und bietet Ihnen eine Reihe von C++-Makros mit denen Sie C++-Code annotieren können. Sie können deklarieren, welche Funktionen, Enums, oder Werttypen, die Sie aus JavaScript verwenden möchten. Los gehts! 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 nehmen wir emscripten.h
nicht mehr auf, da
müssen wir unsere Funktionen nicht mehr mit EMSCRIPTEN_KEEPALIVE
annotieren.
Stattdessen haben wir einen EMSCRIPTEN_BINDINGS
-Abschnitt, in dem wir die Namen unter
mit dem wir unsere Funktionen JavaScript aussetzen möchten.
Um diese Datei zu kompilieren, können wir das gleiche Setup (oder, wenn Sie möchten, die gleichen
Docker-Image) wie im vorherigen Beispiel
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, die unsere erstelltes Wasm-Modul:
<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, wird cwrap()
nicht mehr verwendet. Das funktioniert einfach
der Verpackung. Und was noch wichtiger ist: Wir müssen uns nicht mehr darum kümmern,
Speicherblöcken, damit Zeichenfolgen funktionieren! Mit embind ist das kostenlos, zusammen mit
mit Typprüfungen:
![Entwicklertools-Fehler, wenn eine Funktion mit der falschen Anzahl von Argumenten aufgerufen wird
oder die Argumente haben
Typ](https://web.dev/static/articles/embind/image/devtools-errors-you-invo-b853dbd400744.png?authuser=6&hl=de)
Das ist ziemlich gut, da wir Fehler frühzeitig erkennen können, anstatt uns mit den Problemen die gelegentlich ziemlich unübersichtlichen Wasm-Fehler.
Objekte
Viele JavaScript-Konstruktoren und -Funktionen verwenden Optionsobjekte. Es ist eine schöne in JavaScript, aber es war äußerst mühsam, dies manuell in Wasm zu erkennen. Embind kann auch hier helfen!
Zum Beispiel habe ich eine unglaublich nützliche C++-Funktion entwickelt, mit der mein und ich möchte es dringend im Web nutzen. Das habe ich so gemacht:
#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(), copy.end());
}
if(opts.exclaim) {
copy += "!";
}
std::string acc = std::string("");
for(int i = 0; i < opts.repeat; i++) {
acc += copy;
}
return acc;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<ProcessMessageOpts>("ProcessMessageOpts")
.field("reverse", &ProcessMessageOpts::reverse)
.field("exclaim", &ProcessMessageOpts::exclaim)
.field("repeat", &ProcessMessageOpts::repeat);
function("processMessage", &processMessage);
}
Ich definiere eine Struktur für die Optionen meiner processMessage()
-Funktion. Im
EMSCRIPTEN_BINDINGS
block, kann ich mithilfe von value_object
JavaScript erkennen,
diesen C++-Wert als Objekt. Ich könnte auch value_array
verwenden, wenn ich
verwenden Sie diesen C++-Wert als Array. Ich verbinde auch die Funktion processMessage()
und
der Rest ist magisch. Ich kann die Funktion processMessage()
jetzt über
JavaScript ohne Boilerplate-Code:
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat: 3
}
)); // Prints "hello world!hello world!hello world!"
Klassen
Der Vollständigkeit halber sollte ich Ihnen auch zeigen, wie Sie durch Kombind und eine hohe Synergie mit den ES6-Kursen. Sie können wahrscheinlich bereits ein Muster erkennen:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
class Counter {
public:
int counter;
Counter(int init) :
counter(init) {
}
void increase() {
counter++;
}
int squareCounter() {
return counter * counter;
}
};
EMSCRIPTEN_BINDINGS(my_module) {
class_<Counter>("Counter")
.constructor<int>()
.function("increase", &Counter::increase)
.function("squareCounter", &Counter::squareCounter)
.property("counter", &Counter::counter);
}
JavaScript fühlt sich fast wie eine native Klasse an:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const c = new Module.Counter(22);
console.log(c.counter); // prints 22
c.increase();
console.log(c.counter); // prints 23
console.log(c.squareCounter()); // prints 529
};
</script>
Und was ist mit C?
embind wurde für C++ geschrieben und kann nur in C++-Dateien verwendet werden.
bedeutet, dass Sie keine Links zu C-Dateien erstellen können! Um C und C++ zu mischen, müssen Sie nur
teilen Sie Ihre Eingabedateien in zwei Gruppen auf: eine für C-Dateien und eine für C++-Dateien.
So erweitern Sie die Befehlszeilen-Flags für emcc
:
$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp
Fazit
Mit embind lässt sich die Arbeitsumgebung der Entwicklerinnen und Entwickler mit Wasm und C/C++. In diesem Artikel werden nicht alle Optionen in Kombination mit Angeboten behandelt. Wenn Sie Interesse haben, empfehle ich Ihnen, mit embind's Dokumentation. Beachten Sie, dass die Verwendung von embind dazu führen kann, dass sowohl Ihr Wasm-Modul als auch Ihr JavaScript-Glue Code wird im GZIP-Format um bis zu 11 KB größer, besonders bei kleinen Module. Wenn Sie nur eine sehr kleine Wasm-Oberfläche haben, kostet eine Kombination unter Umständen mehr als in einer Produktionsumgebung lohnt werden. Trotzdem sollten Sie auf jeden Fall Probieren Sie es aus.