Il lie le code JavaScript à votre wasm.
Dans mon dernier article de Wasm, j'ai parlé de la compilation d'une bibliothèque C en Wasm afin de pouvoir l'utiliser sur le Web. Une chose qui m'a le plus marquée (et pour de nombreux lecteurs) est la manière grossière et légèrement 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']),
};
Nous déclarons ici 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 n'est pas compatible avec les chaînes et nécessite de déplacer manuellement des blocs de mémoire, ce qui rend l'utilisation de nombreuses API de bibliothèque très fastidieuses. N'existe-t-il pas une meilleure solution ? Pourquoi oui ? Sinon, quel est le sujet de 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 d'objet pour que l'éditeur de liens 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()
.
En revanche, C++ prend en charge la surcharge des 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 type différent). Au niveau du compilateur, un nom descriptif comme add
serait mangle dans un élément 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 le code
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 voir, nous n'utilisons plus cwrap()
. Cette solution est prête à l'emploi. 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 :
C'est plutôt génial, car nous pouvons détecter certaines erreurs tôt au lieu de nous occuper des erreurs Wasm parfois assez gênantes.
Objets
De nombreux constructeurs et fonctions JavaScript utilisent des objets options. Il s'agit d'un joli modèle en JavaScript, mais extrêmement fastidieux à réaliser manuellement dans Wasm. embind peut également être utile dans ce cas !
Par exemple, j'ai créé 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 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 le bloc EMSCRIPTEN_BINDINGS
, je peux utiliser value_object
pour que JavaScript voit 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()
, et le reste est magique. 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
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 à observer une tendance à 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);
}
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 des fichiers C++. Toutefois, cela ne signifie pas que vous ne pouvez pas créer de lien avec 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++. Ensuite, 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 du développeur lorsqu'il travaille avec wasm et C/C++. Cet article ne couvre pas toutes les options proposées par embind. Si vous êtes intéressé, nous vous recommandons 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 générés au format gzip, notamment sur de petits modules. Si vous ne disposez que d'une très petite surface de gaspillage, l'utilisation d'embind peut coûter plus cher que ce qu'elle vaut dans un environnement de production. Néanmoins, vous devriez absolument essayer.