Emscripten

Il lie JS à votre Wasm !

Dans mon dernier article de Wasm, sur la façon de compiler une bibliothèque C en Wasm afin de pouvoir l’utiliser sur le Web. Une chose qui m'a le plus marqué (et de nombreux lecteurs) est la méthode grossière et légèrement gênante vous devez 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']),
};

Nous déclarons ici les noms des fonctions que nous avons marquées EMSCRIPTEN_KEEPALIVE, quels sont les types de retours et de valeurs arguments. Nous pouvons ensuite utiliser les méthodes de l'objet api pour appeler ces fonctions. Cependant, l'utilisation de Wasm n'est pas compatible avec les chaînes et vous oblige à déplacer manuellement des blocs de mémoire, ce qui rend de nombreuses bibliothèques et très fastidieuses à utiliser. N'existe-t-il pas une meilleure solution ? Pourquoi oui ? Sinon, quel est l'objet de cet article ?

Gestion des noms C++

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

En revanche, C++ prend en charge la surcharge des fonctions, ce qui signifie que vous pouvez implémenter plusieurs fois la même fonction, tant que la signature est différente (par exemple, paramètres de type différent). Au niveau du compilateur, un nom sympa comme add serait mangle dans un élément qui encode la signature dans la fonction. de l'éditeur de liens. Par conséquent, nous ne pourrions pas rechercher notre fonction avec son nom.

Saisissez le code

embiner fait partie de la chaîne d'outils Emscripten et fournit de nombreuses macros C++ qui vous permettent d'annoter du code C++. Vous pouvez déclarer les fonctions, les énumérations ou les types de valeurs que vous prévoyez d'utiliser à partir de JavaScript. C'est parti ! 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. Au lieu de cela, nous avons une section EMSCRIPTEN_BINDINGS dans laquelle nous répertorions les noms sous que nous voulons exposer nos fonctions en JavaScript.

Pour compiler ce fichier, nous pouvons utiliser la même configuration (ou, si vous le souhaitez, la même l'image Docker) comme dans la section pour en savoir plus. 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 créé le module Wasm:

<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 voir, nous n'utilisons plus cwrap(). Cela fonctionne directement de la boîte. Mais surtout, nous n'avons pas à nous soucier de copier manuellement morceaux de mémoire pour que les chaînes fonctionnent ! avec embind, c'est sans frais avec les vérifications de type:

Erreurs des outils de développement lorsque vous appelez une fonction avec un nombre incorrect d&#39;arguments
ou que les arguments n&#39;ont pas
Type

C'est plutôt génial, car nous pouvons détecter quelques erreurs tôt au lieu de nous occuper de des erreurs de Wasm parfois difficiles à manier.

Objets

De nombreux constructeurs et fonctions JavaScript utilisent des objets options. C'est une belle en JavaScript, mais très fastidieux à réaliser manuellement dans Wasm. Embind peut vous aider ici aussi !

Par exemple, j'ai créé cette fonction C++ incroyablement utile qui traite et je veux 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 un struct pour les options de ma fonction processMessage(). Dans EMSCRIPTEN_BINDINGS, je peux utiliser value_object pour que JavaScript voie cette valeur C++ en tant qu'objet. Je pourrais également utiliser value_array si je préfère utiliser cette valeur C++ en tant que tableau. Je lie également la fonction processMessage(). le reste est magique. 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 commencez à constater une tendance:

#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 du C ?

embind a été écrit pour C++ et ne peut être utilisé que dans les fichiers C++, signifier que vous ne pouvez pas créer de lien vers des fichiers C ! Pour combiner les langages C et C++, il suffit de séparez vos fichiers d'entrée en deux groupes: un pour les fichiers C et un pour les fichiers C++. augmentez les options 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 améliore considérablement l'expérience des développeurs avec Wasm et C/C++. Cet article ne décrit pas toutes les options associées. Si vous êtes intéressé, nous vous recommandons de continuer avec embind documentation. Gardez à l'esprit que l'utilisation d'embind peut rendre à la fois votre module Wasm et votre Code Glue JavaScript jusqu'à 11 Ko lorsqu'il est compressé au format gzip, surtout sur de petits modules. Si vous n'avez qu'une très petite surface de gaspillage, l'embind peut coûter plus cher que dans un environnement de production. Néanmoins, vous devez absolument donner essayez.