Emscriptens embind

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

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.