Emscripten

Il lie JS à votre wasm !

Dans mon dernier article sur wasm, j'ai expliqué comment compiler une bibliothèque C en wasm pour pouvoir l'utiliser sur le Web. Une chose qui m'a frappé (et qui a frappé de nombreux lecteurs) est la façon grossière 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 sur l'objet api pour appeler ces fonctions. Toutefois, l'utilisation de wasm de cette manière ne prend pas en charge 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. N'existe-t-il pas une meilleure façon de procéder ? Oui, sinon de quoi parlerait cet article ?

C++ name mangling

Si l'expérience des développeurs était 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 regrouper tous ces fichiers objets et de les transformer en fichier wasm. Avec C, les noms des fonctions sont toujours disponibles dans le fichier objet pour que l'éditeur de liens puisse les utiliser. Pour pouvoir appeler une fonction C, il vous suffit de connaître son nom, que nous fournissons sous forme de chaîne à cwrap().

C++ 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 types différents). Au niveau du compilateur, un nom agréable comme add serait mélangé en quelque chose qui encode la signature dans le nom de la fonction pour l'éditeur de liens. Par conséquent, nous ne pourrons plus rechercher notre fonction par son nom.

Saisir un embind

embind fait partie de la chaîne d'outils Emscripten et vous fournit un ensemble de macros C++ qui vous permettent d'annoter le code C++. Vous pouvez déclarer les fonctions, les énumérations, les classes ou les 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. Nous avons plutôt 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'option --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="/https/web.developers.google.cn/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 voir, nous n'utilisons plus cwrap(). Il fonctionne directement, sans configuration particulière. Mais plus important encore, nous n'avons pas à nous soucier de la copie manuelle de blocs de mémoire pour faire fonctionner les chaînes. embind vous offre cela sans frais, ainsi que des vérifications de type :

Erreurs des outils pour les développeurs lorsque vous appelez une fonction avec un nombre d&#39;arguments incorrect ou lorsque les arguments ont un type incorrect

C'est plutôt bien, car nous pouvons détecter certaines erreurs tôt au lieu de traiter les erreurs Wasm parfois assez difficiles à gérer.

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 à réaliser manuellement dans wasm. embind peut également vous aider ici !

Par exemple, j'ai créé cette fonction C++ incroyablement utile qui traite mes chaînes, et je veux 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 embind. Je peux maintenant appeler la fonction processMessage() à partir de JavaScript sans aucun code passe-partout :

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 apporte beaucoup de synergie avec les classes ES6. Vous avez probablement commencé à remarquer un schéma :

#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é 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 l'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 indicateurs 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 des développeurs lorsqu'ils travaillent 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 glue JavaScript jusqu'à 11 ko lorsqu'ils sont compressés avec gzip, en particulier pour les petits modules. Si vous n'avez qu'une très petite surface wasm, embind peut coûter plus cher qu'il ne vaut dans un environnement de production. Néanmoins, vous devriez absolument l'essayer.