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:
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 du code glue JavaScript jusqu'à 11 Ko lorsqu'il est compressé au format gzip, et plus particulièrement sur les 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.