Emscripten

Il lie JS à votre wasm.

Dans mon dernier article sur le wasm, j'ai expliqué comment compiler une bibliothèque C en wasm afin de pouvoir l'utiliser sur le Web. Une chose qui m'a particulièrement marqué (et à de nombreux lecteurs) est la façon rudimentaire et légèrement maladroite dont vous devez déclarer manuellement les fonctions de votre module wasm que vous utilisez. Pour rappel, voici l'extrait de code dont je parle:

const api = {
    version: Module.cwrap('version', 'number', []),
    create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
    destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};

Ici, nous déclarons les noms des fonctions que nous avons marquées avec EMSCRIPTEN_KEEPALIVE, leurs types de retour et les types de leurs arguments. Ensuite, nous pouvons utiliser les méthodes de l'objet api pour appeler ces fonctions. Toutefois, l'utilisation de wasm de cette manière n'est pas compatible avec les chaînes et vous oblige à déplacer manuellement des blocs de mémoire, ce qui rend de nombreuses API de bibliothèque très fastidieuses à utiliser. Y a-t-il une meilleure façon de procéder ? Bien sûr, sinon, à quoi servirait cet article ?

Mangling de nom en C++

Bien que l'expérience du développeur soit une raison suffisante pour créer un outil qui aide à ces liaisons, il existe en fait une raison plus urgente: lorsque vous compilez du code C ou C++, chaque fichier est compilé séparément. Ensuite, un éditeur de liens se charge de rassembler tous ces fichiers d'objets et de les transformer en fichier wasm. Avec C, les noms des fonctions sont toujours disponibles dans le fichier objet pour que le liant puisse les utiliser. Tout ce dont vous avez besoin pour pouvoir appeler une fonction C est son nom, que nous fournissons sous forme de chaîne à cwrap().

C++, en revanche, prend en charge la surcharge de fonction, ce qui signifie que vous pouvez implémenter la même fonction plusieurs fois tant que la signature est différente (par exemple, des paramètres de type différent). Au niveau du compilateur, un nom agréable comme add est tronqué en quelque chose qui encode la signature dans le nom de la fonction pour le linker. Par conséquent, nous ne pourrions plus rechercher notre fonction par son nom.

Entrer embind

embind fait partie de la chaîne d'outils Emscripten et fournit un ensemble de macros C++ qui vous permettent d'annoter du code C++. Vous pouvez déclarer les fonctions, énumérations, classes ou types de valeurs que vous prévoyez d'utiliser à partir de JavaScript. Commençons par des fonctions simples:

#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);
}

Par rapport à mon article précédent, nous n'incluons plus emscripten.h, car nous n'avons plus besoin d'annoter nos fonctions avec EMSCRIPTEN_KEEPALIVE. À la place, nous avons une section EMSCRIPTEN_BINDINGS dans laquelle nous listons les noms sous lesquels nous souhaitons exposer nos fonctions à JavaScript.

Pour compiler ce fichier, nous pouvons utiliser la même configuration (ou, si vous le souhaitez, la même image Docker) que dans l'article précédent. Pour utiliser embind, nous ajoutons l'indicateur --bind:

$ emcc --bind -O3 add.cpp

Il ne reste plus qu'à créer un fichier HTML qui charge notre module wasm fraîchement créé:

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    console.log(Module.add(1, 2.3));
    console.log(Module.exclaim("hello world"));
};
</script>

Comme vous pouvez le constater, nous n'utilisons plus cwrap(). Cela fonctionne directement. Mais plus important encore, nous n'avons pas à nous soucier de copier manuellement des blocs de mémoire pour que les chaînes fonctionnent. embind vous offre cela sans frais, ainsi que des vérifications de type:

Erreurs DevTools lorsque vous appelez une fonction avec un nombre incorrect d&#39;arguments ou que les arguments sont de type incorrect

C'est très utile, car nous pouvons détecter certaines erreurs à un stade précoce au lieu de devoir gérer les erreurs wasm parfois très difficiles à gérer.

Objets

De nombreux constructeurs et fonctions JavaScript utilisent des objets d'options. Il s'agit d'un bon modèle en JavaScript, mais extrêmement fastidieux à réaliser manuellement dans wasm. embind peut également vous aider.

Par exemple, j'ai conçu cette fonction C++ incroyablement utile qui traite mes chaînes, et je souhaite l'utiliser de toute urgence sur le Web. Voici comment j'ai procédé:

#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);
}

Je définis une structure pour les options de ma fonction processMessage(). Dans le bloc EMSCRIPTEN_BINDINGS, je peux utiliser value_object pour que JavaScript considère cette valeur C++ comme un objet. Je pourrais également utiliser value_array si je préférais utiliser cette valeur C++ comme tableau. Je lie également la fonction processMessage(), et le reste est de la magie d'embind. Je peux maintenant appeler la fonction processMessage() à partir de JavaScript sans code standard:

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

Classes

Pour être complet, je dois également vous montrer comment embind vous permet d'exposer des classes entières, ce qui génère beaucoup de synergies avec les classes ES6. Vous pouvez probablement commencer à voir un schéma à ce stade:

#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);
}

Côté JavaScript, cela ressemble presque à une classe native:

<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>

Qu'en est-il de C ?

embind a été écrit pour C++ et ne peut être utilisé que dans des fichiers C++, mais cela ne signifie pas que vous ne pouvez pas associer des fichiers C. Pour mélanger C et C++, il vous suffit de séparer vos fichiers d'entrée en deux groupes: un pour les fichiers C et un pour les fichiers C++, et d'augmenter les options de la CLI pour emcc comme suit:

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

Conclusion

embind améliore considérablement l'expérience du développeur lorsqu'il travaille avec wasm et C/C++. Cet article ne couvre pas toutes les options proposées par embind. Si cela vous intéresse, je vous recommande de consulter la documentation d'embind. N'oubliez pas que l'utilisation d'embind peut augmenter la taille de votre module wasm et de votre code de liaison JavaScript jusqu'à 11 ko lorsqu'il est compressé avec gzip, en particulier sur les petits modules. Si vous ne disposez que d'une très petite surface WASM, embind peut coûter plus cher que prévu dans un environnement de production. Néanmoins, vous devriez absolument essayer.