Emscripten

Cela lie JS à votre Wasm.

Dans mon dernier article sur Wasm, j'ai expliqué comment compiler une bibliothèque C dans Wasm afin de pouvoir l'utiliser sur le Web. Pour moi, comme pour de nombreux lecteurs, j'ai particulièrement apprécié la manière grossière et un peu gênante de déclarer manuellement les fonctions de votre module Wasm que vous utilisez. Pour vous rafraîchir la mémoire, 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 renvoyés et les types de leurs arguments. Nous pouvons ensuite utiliser les méthodes de l'objet api pour appeler ces fonctions. Cependant, l'utilisation de Wasm de cette manière n'est pas compatible avec les chaînes et nécessite de déplacer manuellement des fragments de mémoire, ce qui rend l'utilisation de nombreuses API de bibliothèque très fastidieuses. Existe-t-il un meilleur moyen ? Pourquoi oui ? Sinon, quel est le sujet de cet article ?

Gestion des noms C++

Bien que l'expérience développeur soit une raison suffisante pour créer un outil permettant de faciliter ces liaisons, il existe en fait une raison plus pressante: lorsque vous compilez du code C ou C++, chaque fichier est compilé séparément. Ensuite, un linker 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 d'objet pour que l'éditeur de liens puisse les utiliser. Tout ce dont vous avez besoin pour appeler une fonction C est son nom, que nous fournissons à cwrap() sous forme de chaîne.

C++, en revanche, est compatible avec la surcharge de fonctions, ce qui signifie que vous pouvez implémenter la même fonction plusieurs fois tant que la signature est différente (par exemple, avec des paramètres de types différents). Au niveau du compilateur, un nom intéressant comme add serait mangle en quelque chose qui encode la signature dans le nom de la fonction de l'éditeur de liens. Par conséquent, nous ne pourrions plus rechercher notre fonction avec son nom.

Saisissez embind

embind fait partie de la chaîne d'outils Emscripten et vous fournit de nombreuses macros C++ qui vous permettent d'annoter du code C++. Vous pouvez déclarer à partir de JavaScript les fonctions, énumérations, classes ou types de valeurs que vous prévoyez d'utiliser. Commençons simple avec quelques 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 répertorions 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 vous reste plus qu'à créer un fichier HTML qui charge notre module Wasm nouvellement 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(). Cette solution est prête à l'emploi. Mais surtout, nous n'avons pas à nous soucier de copier manuellement des fragments de mémoire pour faire fonctionner les chaînes. embind vous offre cette possibilité, ainsi que la vérification du type:

Erreurs des outils de développement lorsque vous appelez une fonction avec un nombre incorrect d&#39;arguments ou lorsque le type d&#39;arguments est incorrect

C'est très utile, car nous pouvons détecter certaines erreurs plus tôt au lieu de gérer les erreurs Wasm parfois assez pénibles.

Objets

De nombreux constructeurs et fonctions JavaScript utilisent des objets d'options. Il s'agit d'un modèle intéressant en JavaScript, mais extrêmement fastidieux à appliquer manuellement dans Wasm. embind peut aussi être utile ici.

Par exemple, j'ai créé cette fonction C++ incroyablement utile qui traite mes chaînes de caractères, et je souhaite de toute urgence l'utiliser sur le Web. Voici comment j'ai fait:

#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 faire en sorte que JavaScript considère cette valeur C++ comme un objet. Je pourrais également utiliser value_array si je préfère utiliser cette valeur C++ en tant que tableau. J'associe également la fonction processMessage(), et le reste est embiné par la magic. Je peux maintenant appeler la fonction processMessage() à partir de JavaScript sans code récurrent:

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

Classes

Par souci d'exhaustivité, je dois également vous montrer comment embind vous permet d'exposer des classes entières, ce qui offre une grande synergie avec les classes ES6. Vous pouvez probablement commencer à voir un modèle à présent:

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

Du côté de 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>

Et le 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 créer de lien vers des fichiers C. Pour mélanger les langages 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 indicateurs de 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 vous offre d'importantes améliorations de l'expérience des développeurs lorsque vous utilisez Wasm et C/C++. Cet article ne couvre pas toutes les options embind des offres. Si cela vous intéresse, nous vous recommandons de consulter la documentation d'embind. Gardez à l'esprit que l'utilisation d'embind peut augmenter la taille de votre module Wasm et de votre code JavaScript Glue jusqu'à 11 Ko lorsqu'il est gzip’d, notamment pour les petits modules. Si vous n'avez qu'une très petite surface de Wasm, embind peut coûter plus cher que ce qu'elle vaut dans un environnement de production. Néanmoins, vous devriez vraiment essayer.