Dans la section Qu'est-ce que WebAssembly et d'où vient-il ?, J'ai expliqué comment nous en sommes arrivés à la WebAssembly d'aujourd'hui. Dans cet article, je vais vous montrer comment compiler un programme C existant, mkbitmap
, pour WebAssembly. Il est plus complexe que l'exemple hello world, car il permet de travailler avec des fichiers, de communiquer entre les emplacements WebAssembly et JavaScript, et de dessiner sur un canevas, tout en restant suffisamment gérable pour ne pas vous submerger.
Cet article s'adresse aux développeurs Web qui souhaitent apprendre à utiliser WebAssembly et présente les étapes à suivre si vous souhaitez compiler un élément comme mkbitmap
vers WebAssembly. Sachez qu'il est tout à fait normal de ne pas compiler une application ou une bibliothèque lors de la première exécution. C'est pourquoi certaines des étapes décrites ci-dessous n'ont pas fonctionné. J'ai donc dû revenir en arrière et réessayer. L'article ne montre pas la commande de compilation finale magique comme si elle était tombée du ciel, mais décrit plutôt ma progression réelle, y compris quelques frustrations.
Environ mkbitmap
Le programme C mkbitmap
lit une image et lui applique une ou plusieurs des opérations suivantes, dans cet ordre: inversion, filtrage passe-haut, mise à l'échelle et seuil. Chaque opération peut être contrôlée, activée ou désactivée individuellement. mkbitmap
est principalement utilisé pour convertir des images en couleurs ou en nuances de gris dans un format adapté à d'autres programmes, en particulier le programme de traçage potrace
qui constitue la base du SVGcode. En tant qu'outil de prétraitement, mkbitmap
est particulièrement utile pour convertir des dessins au trait numérisés, tels que des dessins animés ou du texte manuscrit, en images haute résolution à deux niveaux.
Pour utiliser mkbitmap
, transmettez-lui un certain nombre d'options et un ou plusieurs noms de fichiers. Pour en savoir plus, consultez la page de manuel de l'outil:
$ mkbitmap [options] [filename...]
![Dessin animé en couleur.](https://web.dev/static/articles/compiling-mkbitmap-to-webassembly/image/cartoon-image-color.png?authuser=6&hl=fr)
![Image de dessin animé convertie en nuances de gris après le prétraitement.](https://web.dev/static/articles/compiling-mkbitmap-to-webassembly/image/cartoon-image-converted-97142bc66e12e.png?authuser=6&hl=fr)
mkbitmap -f 2 -s 2 -t 0.48
(source).Obtenir le code
La première étape consiste à obtenir le code source de mkbitmap
. Vous le trouverez sur le site Web du projet. Au moment de la rédaction de ce document, potrace-1.16.tar.gz est la dernière version.
Compiler et installer localement
L'étape suivante consiste à compiler et à installer l'outil localement pour avoir une idée de son comportement. Le fichier INSTALL
contient les instructions suivantes:
cd
vers le répertoire contenant le code source du package, puis saisissez./configure
pour configurer le package pour votre système.L'exécution de
configure
peut prendre un certain temps. Lors de l'exécution, il affiche des messages indiquant les fonctionnalités qu'il recherche.Saisissez
make
pour compiler le package.Si vous le souhaitez, saisissez
make check
pour exécuter les tests automatiques fournis avec le package, généralement à l'aide des binaires désinstallés qui viennent d'être créés.Saisissez
make install
pour installer les programmes, ainsi que tous les fichiers de données et la documentation. Lors de l'installation dans un préfixe appartenant à l'utilisateur racine, il est recommandé de configurer et de créer le package en tant qu'utilisateur standard, et d'exécuter uniquement la phasemake install
avec des privilèges racine.
En suivant ces étapes, vous devriez obtenir deux exécutables, potrace
et mkbitmap
. Ce dernier est l'objet de cet article. Vous pouvez vérifier que tout a bien fonctionné en exécutant mkbitmap --version
. Voici le résultat des quatre étapes de ma machine, bien abrégées par souci de concision:
Étape 1, ./configure
:
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands
Étape 2, make
:
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.
Étape 3, make check
:
$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS: 8
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.
Étape 4, sudo make install
:
$ sudo make install
Password:
Making install in src
.././install-sh -c -d '/usr/local/bin'
/bin/sh ../libtool --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.
Pour vérifier si cela a fonctionné, exécutez mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Si vous obtenez les détails de la version, cela signifie que vous avez correctement compilé et installé mkbitmap
. Ensuite, faites en sorte que l'équivalent de ces étapes fonctionne avec WebAssembly.
Compiler mkbitmap
dans WebAssembly
Emscripten est un outil permettant de compiler des programmes C/C++ pour WebAssembly. La documentation d'Emmscripten sur la création de projets indique ce qui suit:
Créer des projets de grande envergure avec Emscripten est un jeu d'enfant. Emscripten fournit deux scripts simples qui configurent vos fichiers makefile pour utiliser
emcc
comme solution de remplacement prête à l'emploi pourgcc
. Dans la plupart des cas, le reste du système de compilation actuel de votre projet reste inchangé.
La documentation continue (un peu modifiée par souci de concision):
Prenons le cas où vous compilez normalement avec les commandes suivantes:
./configure
make
Pour compiler avec Emscripten, utilisez plutôt les commandes suivantes:
emconfigure ./configure
emmake make
Ainsi, ./configure
devient emconfigure ./configure
et make
devient emmake make
. Le code suivant montre comment procéder avec mkbitmap
.
Étape 0, make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
Étape 1, emconfigure ./configure
:
$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands
Étape 2, emmake make
:
$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.
Si tout s'est bien passé, le répertoire devrait maintenant contenir des fichiers .wasm
. Pour les trouver, exécutez find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
Les deux derniers semblent prometteurs. cd
dans le répertoire src/
. Il existe également deux nouveaux fichiers correspondants : mkbitmap
et potrace
. Pour cet article, seul mkbitmap
est pertinent. Le fait qu'ils ne comportent pas l'extension .js
est un peu déroutant, mais il s'agit en fait de fichiers JavaScript, vérifiables à l'aide d'un appel head
rapide:
$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
Renommez le fichier JavaScript mkbitmap.js
en appelant mv mkbitmap mkbitmap.js
(et mv potrace potrace.js
respectivement).
Le premier test est à présent nécessaire pour vérifier si le code a fonctionné. Pour ce faire, exécutez le fichier avec Node.js dans la ligne de commande en exécutant node mkbitmap.js --version
:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Vous venez de compiler mkbitmap
dans WebAssembly. L'étape suivante consiste à le faire fonctionner dans le navigateur.
mkbitmap
avec WebAssembly dans le navigateur
Copiez les fichiers mkbitmap.js
et mkbitmap.wasm
dans un nouveau répertoire appelé mkbitmap
, puis créez un fichier récurrent HTML index.html
qui charge le fichier JavaScript mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<script src="mkbitmap.js"></script>
</body>
</html>
Démarrez un serveur local qui diffuse le répertoire mkbitmap
et ouvrez-le dans votre navigateur. Une invite de saisie devrait s'afficher. C'est normal, car, selon la page de manuel de l'outil, "[i]si aucun argument de nom de fichier n'est fourni, mkbitmap agit comme un filtre, lisant à partir de l'entrée standard", qui pour Emscripten est par défaut un prompt()
.
Empêcher l'exécution automatique
Pour empêcher mkbitmap
de s'exécuter immédiatement et de le faire attendre l'entrée utilisateur, vous devez comprendre l'objet Module
d'Emscripten. Module
est un objet JavaScript global avec des attributs que le code généré par Emscripten appelle à différents moments de son exécution.
Vous pouvez fournir une implémentation de Module
pour contrôler l'exécution du code.
Lorsqu'une application Emscripten démarre, elle examine les valeurs de l'objet Module
et les applique.
Dans le cas de mkbitmap
, définissez Module.noInitialRun
sur true
pour empêcher l'exécution initiale qui a entraîné l'affichage de l'invite. Créez un script appelé script.js
, incluez-le avant <script src="mkbitmap.js"></script>
dans index.html
et ajoutez le code suivant à script.js
. Lorsque vous actualisez l'application, l'invite doit disparaître.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Créer une compilation modulaire avec quelques indicateurs de compilation supplémentaires
Pour fournir des entrées à l'application, vous pouvez utiliser la compatibilité du système de fichiers d'Emmscripten dans Module.FS
. La section Inclure la prise en charge du système de fichiers indique:
Emscripten décide d'inclure automatiquement ou non la prise en charge du système de fichiers. De nombreux programmes n'ont pas besoin de fichiers, et le système de fichiers pris en charge n'est pas négligeable en taille. Emscripten évite donc de l'inclure lorsqu'il n'y voit aucune raison. Cela signifie que si votre code C/C++ n'accède pas aux fichiers, l'objet
FS
et les autres API de système de fichiers ne seront pas inclus dans la sortie. En revanche, si votre code C/C++ utilise des fichiers, la prise en charge du système de fichiers sera automatiquement incluse.
Malheureusement, mkbitmap
est l'un des cas où Emscripten n'inclut pas automatiquement la compatibilité avec les systèmes de fichiers. Vous devez donc l'indiquer explicitement. Vous devez donc suivre les étapes emconfigure
et emmake
décrites précédemment, avec quelques indicateurs supplémentaires définis via un argument CFLAGS
. Les options suivantes peuvent également s'avérer utiles pour d'autres projets.
- Définissez
-sFILESYSTEM=1
pour inclure la compatibilité avec le système de fichiers. - Définissez
-sEXPORTED_RUNTIME_METHODS=FS,callMain
pour queModule.FS
etModule.callMain
soient exportés. - Définissez
-sMODULARIZE=1
et-sEXPORT_ES6
pour générer un module ES6 moderne. - Définissez
-sINVOKE_RUN=0
pour empêcher l'exécution initiale ayant entraîné l'affichage de l'invite.
Dans ce cas particulier, vous devez également définir l'option --host
sur wasm32
pour indiquer au script configure
que vous compilez pour WebAssembly.
La commande emconfigure
finale se présente comme suit:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
N'oubliez pas d'exécuter à nouveau emmake make
et de copier les fichiers nouvellement créés dans le dossier mkbitmap
.
Modifiez index.html
pour qu'il ne charge que le module ES script.js
, à partir duquel vous importez ensuite le module mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<!-- No longer load `mkbitmap.js` here -->
<script src="script.js" type="module"></script>
</body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
console.log(Module);
};
run();
Lorsque vous ouvrez l'application dans le navigateur, vous devriez voir l'objet Module
enregistré dans la console DevTools et l'invite a disparu, car la fonction main()
de mkbitmap
n'est plus appelée au démarrage.
Exécuter manuellement la fonction principale
L'étape suivante consiste à appeler manuellement la fonction main()
de mkbitmap
en exécutant Module.callMain()
. La fonction callMain()
utilise un tableau d'arguments qui correspondent individuellement à ce que vous transmettreiez sur la ligne de commande. Si vous exécutez mkbitmap -v
sur la ligne de commande, appelez Module.callMain(['-v'])
dans le navigateur. Le numéro de version mkbitmap
est alors consigné dans la console des outils de développement.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
Rediriger la sortie standard
Par défaut, la sortie standard (stdout
) est la console. Cependant, vous pouvez la rediriger vers un autre élément, par exemple une fonction qui stocke la sortie dans une variable. Cela signifie que vous pouvez ajouter la sortie au code HTML en définissant la propriété Module.print
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
let consoleOutput = 'Powered by ';
const Module = await loadWASM({
print: (text) => (consoleOutput += text),
});
Module.callMain(['-v']);
document.body.textContent = consoleOutput;
};
run();
Extraire le fichier d'entrée dans le système de fichiers de la mémoire
Pour transférer le fichier d'entrée dans le système de fichiers mémoire, vous avez besoin de l'équivalent de mkbitmap filename
sur la ligne de commande. Pour comprendre comment j'aborde ce point, commencez par obtenir quelques informations sur la manière dont mkbitmap
attend ses entrées et crée sa sortie.
Les formats d'entrée compatibles avec mkbitmap
sont PNM (PBM, PGM, PPM) et BMP. Les formats de sortie sont PBM pour les bitmaps et PGM pour les graymaps. Si un argument filename
est fourni, mkbitmap
crée par défaut un fichier de sortie dont le nom est obtenu à partir du nom du fichier d'entrée en remplaçant son suffixe par .pbm
. Par exemple, pour le nom du fichier d'entrée example.bmp
, le nom du fichier de sortie est example.pbm
.
Emscripten fournit un système de fichiers virtuel qui simule le système de fichiers local. Ainsi, le code natif utilisant des API de fichiers synchrones peut être compilé et exécuté avec peu ou pas de modification.
Pour que mkbitmap
puisse lire un fichier d'entrée comme s'il avait été transmis en tant qu'argument de ligne de commande filename
, vous devez utiliser l'objet FS
fourni par Emscripten.
L'objet FS
repose sur un système de fichiers en mémoire (communément appelé MEMFS) et comporte une fonction writeFile()
que vous utilisez pour écrire des fichiers dans le système de fichiers virtuel. Utilisez writeFile()
comme indiqué dans l'exemple de code suivant.
Pour vérifier que l'opération d'écriture du fichier a bien fonctionné, exécutez la fonction readdir()
de l'objet FS
avec le paramètre '/'
. Vous verrez example.bmp
et un certain nombre de fichiers par défaut qui sont toujours créés automatiquement.
Notez que l'appel précédent à Module.callMain(['-v'])
pour imprimer le numéro de version a été supprimé. Cela est dû au fait que Module.callMain()
est une fonction qui ne s'attend généralement qu'à une seule exécution.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
console.log(Module.FS.readdir('/'));
};
run();
Première exécution effective
Une fois que tout est en place, exécutez mkbitmap
en exécutant Module.callMain(['example.bmp'])
. Consignez le contenu du dossier '/'
de MEMFS. Vous devriez voir le fichier de sortie example.pbm
nouvellement créé à côté du fichier d'entrée example.bmp
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
console.log(Module.FS.readdir('/'));
};
run();
Extraire le fichier de sortie du système de fichiers de la mémoire
La fonction readFile()
de l'objet FS
permet d'extraire le example.pbm
créé lors de la dernière étape du système de fichiers de la mémoire. La fonction renvoie un Uint8Array
que vous convertissez en objet File
et enregistrez sur le disque, car les navigateurs ne sont généralement pas compatibles avec les fichiers PBM pour un affichage direct dans le navigateur.
(Il existe des méthodes plus intéressantes pour enregistrer un fichier, mais l'utilisation d'un <a download>
créé dynamiquement est la plus largement acceptée.) Une fois le fichier enregistré, vous pouvez l'ouvrir dans la visionneuse d'images de votre choix.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
const file = new File([output], 'example.pbm', {
type: 'image/x-portable-bitmap',
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = file.name;
a.click();
};
run();
Ajouter une interface utilisateur interactive
À ce stade, le fichier d'entrée est codé en dur et mkbitmap
s'exécute avec les paramètres par défaut. La dernière étape consiste à laisser l'utilisateur sélectionner dynamiquement un fichier d'entrée, modifier les paramètres mkbitmap
, puis exécuter l'outil avec les options sélectionnées.
// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);
Le format d'image PBM n'est pas particulièrement difficile à analyser. Par conséquent, avec du code JavaScript, vous pouvez même afficher un aperçu de l'image de sortie. Pour savoir comment procéder, consultez le code source de la démonstration intégrée ci-dessous.
Conclusion
Félicitations, vous venez de compiler mkbitmap
dans WebAssembly et de l'utiliser dans le navigateur ! Il y a eu des impasses et vous avez dû compiler l'outil plusieurs fois jusqu'à ce que cela fonctionne, mais comme je l'ai indiqué plus haut, cela fait partie de l'expérience. Pensez également à la balise webassembly
de StackOverflow si vous rencontrez des difficultés. Bonne compilation !
Remerciements
Cet article a été lu par Sam Clegg et Rachel Andrew.