Comment intégrer WebAssembly dans cette configuration ? Dans cet article, nous allons résoudre ce problème avec C/C++ et Emscripten, par exemple.
WebAssembly (wasm) est souvent soit conçue comme une primitive de performance, soit comme un moyen d'exécuter votre C++ existant sur le Web. Avec squoosh.app, nous voulions vous montrer qu'il existe au moins une troisième perspective pour le Wasm: exploiter l'énorme des écosystèmes d’autres langages de programmation. Avec Emscripten, vous pouvez utiliser du code C/C++, Compatible avec Wasm, le Go l'équipe travaille aussi dessus. Je suis que beaucoup d'autres langues suivront.
Dans ces scénarios, Wasm n'est pas la pièce maîtresse de votre application, mais plutôt un puzzle. pièce: encore un autre module. Votre application contient déjà du code JavaScript, CSS, image, un système de compilation centré sur le Web et peut-être même un framework comme React. Comment intégrer WebAssembly dans cette configuration ? Dans cet article, nous allons travailler avec C/C++ et Emscripten comme exemple.
Docker
Pour moi, Docker est très utile lorsque je travaille avec Emscripten. C/C++ les bibliothèques sont souvent écrites pour fonctionner avec le système d’exploitation sur lequel elles sont basées. C'est incroyablement utile d'avoir un environnement cohérent. Avec Docker, vous obtenez virtualisé Linux, déjà configuré pour fonctionner avec Emscripten, tous les outils et dépendances installés. S'il manque quelque chose, vous pouvez simplement sans avoir à vous soucier de l'impact sur votre propre machine d'autres projets. En cas de problème, jetez le conteneur terminé. S'il fonctionne une fois, vous pouvez être sûr qu'il continuera à fonctionner et produit des résultats identiques.
Le registre Docker dispose d'un fichier Emscripten image de trzeci que j'utilise beaucoup.
Intégration à npm
Dans la plupart des cas, le point d'entrée d'un projet Web est
package.json
Par convention, la plupart des projets peuvent être créés avec npm install &&
npm run build
.
En général, les artefacts de compilation produits par Emscripten (un .js
et un .wasm
) doivent être traités comme un simple module JavaScript et un autre
élément. Le fichier JavaScript peut être géré par un bundler
comme webpack ou Rollup,
et le fichier Wasm doit être traité comme tout autre élément binaire plus volumineux, comme
images.
Par conséquent, les artefacts de compilation Emscripten doivent être compilés avant votre configuration "normal" le processus de compilation entre en action:
{
"name": "my-worldchanging-project",
"scripts": {
"build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
"build:app": "<the old build command>",
"build": "npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
La nouvelle tâche build:emscripten
pourrait appeler directement Emscripten, mais comme
nous vous recommandons d'utiliser Docker pour vérifier que l'environnement de compilation
cohérentes.
docker run ... trzeci/emscripten ./build.sh
indique à Docker de lancer une nouvelle
à l'aide de l'image trzeci/emscripten
et exécutez la commande ./build.sh
.
build.sh
est un script shell que vous allez écrire ensuite. --rm
raconte
Docker pour supprimer le conteneur une fois son exécution terminée. De cette façon, vous ne construisez pas
une collection d'images système
obsolètes au fil du temps. -v $(pwd):/src
signifie que
que Docker doit "mettre en miroir" le répertoire actuel ($(pwd)
) vers /src
à l'intérieur
le conteneur. Toutes les modifications que vous apportez aux fichiers du répertoire /src
à l'intérieur de
conteneur seront mis en miroir dans votre projet réel. Ces répertoires dupliqués
sont appelés "montages de liaison".
Examinons build.sh
:
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
src/my-module.cpp
# Create output folder
mkdir -p dist
# Move artifacts
mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
Il y a beaucoup de choses à découvrir ici !
set -e
place le shell en mode "fail fast" (échec rapide). . Si des commandes du script
renvoie une erreur, l'ensemble du script est immédiatement abandonné. Il peut s'agir
incroyablement utile car la dernière sortie
du script aura toujours un succès
ou l'erreur qui a causé l'échec de la compilation.
Avec les instructions export
, vous définissez les valeurs de quelques environnements
variables. Ils vous permettent de transmettre des paramètres de ligne de commande supplémentaires au
le compilateur (CFLAGS
), le compilateur C++ (CXXFLAGS
) et l'éditeur de liens (LDFLAGS
).
Ils reçoivent tous les paramètres de l'optimiseur via OPTIMIZE
pour s'assurer que
tout est optimisé de la même manière. Plusieurs valeurs sont possibles
Pour la variable OPTIMIZE
:
-O0
: ne procédez à aucune optimisation. Aucun code mort n'est éliminé, et Emscripten ne réduit pas non plus la taille du code JavaScript qu'il émet. Convient pour le débogage.-O3
: optimiser les performances de manière agressive-Os
: optimisation agressive des performances et de la taille en tant qu'objectif secondaire critère.-Oz
: procéder à une optimisation agressive en fonction de la taille, en sacrifiant les performances si nécessaire.
Pour le Web, je recommande plutôt -Os
.
La commande emcc
possède une multitude d'options. Notez que le fichier emcc est
est censé être un "remplacement intégré pour les compilateurs tels que GCC ou clang". Donc tous
que vous connaissez peut-être auprès de GCC seront très probablement implémentés par emcc
bien. L'option -s
est spéciale, car elle nous permet de configurer Emscripten
en particulier. Vous trouverez toutes les options disponibles dans
settings.js
,
mais ce fichier peut
être assez écrasant. Voici la liste des indicateurs Emscripten
qui, selon moi, sont les plus importants pour les développeurs Web:
--bind
activations embind.-s STRICT=1
ne prend plus en charge toutes les options de compilation obsolètes. Cela garantit que votre code construit de manière compatible avec les versions ultérieures.-s ALLOW_MEMORY_GROWTH=1
permet d'augmenter automatiquement la mémoire si nécessaires. Au moment de la rédaction de ce document, Emscripten alloue 16 Mo de mémoire au départ. Lorsque votre code alloue des fragments de mémoire, cette option détermine si ces opérations feront échouer l'ensemble du module Wasm lorsque la mémoire est épuisé, ou si le code Glue est autorisé à développer la mémoire totale pour pour l'allocation.-s MALLOC=...
choisit l'implémentation demalloc()
à utiliser.emmalloc
correspond à une implémentationmalloc()
petite et rapide spécialement pour Emscripten. La l'alternative estdlmalloc
, une implémentationmalloc()
complète. Vous uniquement besoin de passer àdlmalloc
si vous allouez de nombreux petits objets fréquemment ou si vous voulez utiliser des threads.-s EXPORT_ES6=1
transforme le code JavaScript en module ES6 avec une Exportation par défaut compatible avec n'importe quel bundler. Nécessite également-s MODULARIZE=1
pour définis.
Les options suivantes ne sont pas toujours nécessaires ou ne sont utiles que pour le débogage finalités:
-s FILESYSTEM=0
est un indicateur lié à Emscripten et permettant de émuler un système de fichiers pour vous lorsque votre code C/C++ utilise des opérations de système de fichiers. Il analyse le code qu'il compile pour décider d'inclure ou non le l'émulation du système de fichiers dans le code glue ou non. Parfois, cependant, peut se tromper et vous payez 70 Ko en colle supplémentaire, pour une émulation de système de fichiers dont vous n'avez peut-être pas besoin. Avec-s FILESYSTEM=0
, vous pouvez forcer Emscripten à ne pas inclure ce code.-g4
obligera Emscripten à inclure des informations de débogage dans.wasm
et émet également un fichier de mappage source pour le module Wasm. Pour en savoir plus sur avec Emscripten dans leur méthode de débogage .
Et voilà ! Pour tester cette configuration, créons un petit my-module.cpp
:
#include <emscripten/bind.h>
using namespace emscripten;
int say_hello() {
printf("Hello from your wasm module\n");
return 0;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("sayHello", &say_hello);
}
Et un index.html
:
<!doctype html>
<title>Emscripten + npm example</title>
Open the console to see the output from the wasm module.
<script type="module">
import wasmModule from "./my-module.js";
const instance = wasmModule({
onRuntimeInitialized() {
instance.sayHello();
}
});
</script>
(Voici un essentiel contenant tous les fichiers).
Pour tout compiler, exécutez
$ npm install
$ npm run build
$ npm run serve
En accédant à localhost:8080, vous devriez obtenir le résultat suivant dans Console des outils de développement:
Ajouter du code C/C++ en tant que dépendance
Si vous souhaitez créer une bibliothèque C/C++ pour votre application Web, son code doit être
de votre projet. Vous pouvez ajouter manuellement le code au dépôt de votre projet
ou vous pouvez également utiliser npm pour
gérer ce type de dépendances. Disons que je
utiliser libvpx dans l'application Web libvpx
est une bibliothèque C++ permettant d'encoder des images avec le codec VP8, utilisé dans les fichiers .webm
.
Cependant, libvpx n'est pas sur npm et n'a pas de package.json
. Je ne peux donc pas
installez-le directement
en utilisant npm.
Pour résoudre ce casse-tête,
Napa. Napa vous permet d'installer n'importe quel
l'URL du dépôt en tant que dépendance dans votre dossier node_modules
.
Installez napa en tant que dépendance:
$ npm install --save napa
Veillez à exécuter napa
en tant que script d'installation:
{
// ...
"scripts": {
"install": "napa",
// ...
},
"napa": {
"libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}
Lorsque vous exécutez npm install
, napa se charge de cloner le fichier GitHub libvpx
dans votre node_modules
, sous le nom libvpx
.
Vous pouvez maintenant étendre votre script de compilation pour compiler libvpx. libvpx utilise configure
et make
à créer. Heureusement, Emscripten peut s'assurer que configure
et
make
utilise le compilateur d'Emscripten. À cette fin, le wrapper
commandes emconfigure
et emmake
:
# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...
Une bibliothèque C/C++ est divisée en deux parties: les en-têtes (généralement .h
ou
.hpp
) qui définissent les structures de données, les classes, les constantes, etc. qu'un
de la bibliothèque et de la bibliothèque proprement dite (généralement des fichiers .so
ou .a
). À
utilisez la constante VPX_CODEC_ABI_VERSION
de la bibliothèque dans votre code, vous obtenez
pour inclure les fichiers d'en-tête de la bibliothèque à l'aide d'une instruction #include
:
#include "vpxenc.h"
#include <emscripten/bind.h>
int say_hello() {
printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
return 0;
}
Le problème est que le compilateur ne sait pas où rechercher vpxenc.h
.
C'est à cela que sert l'indicateur -I
. Il indique au compilateur
quels répertoires
vérifier les fichiers d'en-tête. De plus, vous devez également
donner au compilateur le
fichier de bibliothèque réel:
# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
-I ./node_modules/libvpx \
src/my-module.cpp \
build-vpx/libvpx.a
# ... below is unchanged ...
Si vous exécutez npm run build
maintenant, vous verrez que le processus crée une nouvelle .js
.
et un nouveau fichier .wasm
, et que la page de démonstration renverra bien la constante:
Vous remarquerez également que le processus de compilation prend beaucoup de temps. La raison pour laquelle
les longues durées de compilation peuvent varier. Dans le cas de libvpx, l'opération prend beaucoup de temps, car
il compile un encodeur et un décodeur pour VP8 et VP9 chaque fois que vous exécutez
votre commande build, même si les fichiers sources n'ont pas changé. Même un petit
La compilation d'une modification de votre my-module.cpp
prendra beaucoup de temps. Il serait très
utile de conserver les artefacts de compilation de libvpx une fois qu'ils ont été
construit la première fois.
Pour ce faire, vous pouvez utiliser des variables d'environnement.
# ... above is unchanged ...
eval $@
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...
(en bref contenant tous les fichiers).
La commande eval
permet de définir des variables d'environnement en transmettant des paramètres
au script de compilation. La commande test
ignore la compilation de libvpx si
$SKIP_LIBVPX
est défini sur n'importe quelle valeur.
Vous pouvez maintenant compiler votre module, mais sans avoir à recréer libvpx:
$ npm run build:emscripten -- SKIP_LIBVPX=1
Personnaliser l'environnement de compilation
Les bibliothèques ont parfois besoin d'outils supplémentaires pour se compiler. Si ces dépendances
manquants dans l'environnement de compilation fourni par l'image Docker, vous devez
ajoutez-les vous-même. Par exemple, supposons que vous souhaitiez également créer
de libvpx utilisant doxygen. Doxygène n’est pas
disponible dans votre conteneur Docker, mais vous pouvez l'installer à l'aide de apt
.
Si vous le faisiez dans votre build.sh
, vous devez le télécharger à nouveau, puis le réinstaller
doxygène chaque fois que vous
voulez développer votre bibliothèque. Non seulement
ce serait
gaspillage, mais cela vous empêcherait également
de travailler sur votre projet hors ligne.
Dans ce cas, il est judicieux de créer votre propre image Docker. Les images Docker sont créées par
écrire une Dockerfile
qui décrit les étapes de compilation. Les Dockerfiles sont assez
puissant et ont beaucoup de
commandes, mais la plupart des
temps, vous pouvez simplement utiliser FROM
, RUN
et ADD
. Dans ce cas :
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
Avec FROM
, vous pouvez déclarer l'image Docker que vous souhaitez utiliser comme point de départ.
point d'accès. J'ai choisi trzeci/emscripten
comme base (l'image que vous avez utilisée)
depuis le début. Avec RUN
, vous demandez à Docker d'exécuter des commandes shell dans le
conteneur. Les modifications apportées par ces commandes au conteneur font désormais partie
l'image Docker. Pour vous assurer que votre image Docker a été créée et qu'elle est
disponibles avant d'exécuter build.sh
, vous devez ajuster votre package.json
bit:
{
// ...
"scripts": {
"build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
"build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
"build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
(en bref contenant tous les fichiers).
Votre image Docker sera créée, mais seulement si elle n'a pas encore été créée. Ensuite,
Tout s'exécute comme précédemment, mais l'environnement de compilation dispose désormais du doxygen
disponible. La documentation de libvpx sera donc compilée
bien.
Conclusion
Il n'est pas surprenant que le code C/C++ et npm ne soient pas un ajustement naturel, mais vous pouvez pour qu'il fonctionne confortablement, avec d'autres outils et l'isolement fournies par Docker. Cette configuration ne fonctionne pas pour tous les projets, mais il s'agit point de départ correct que vous pouvez adapter à vos besoins. Si vous avez d'améliorations, veuillez les partager.
Annexe: Utiliser les couches d'image Docker
Une autre solution consiste à encapsuler davantage de ces problèmes avec Docker et L'approche intelligente de la mise en cache de Docker Docker exécute les Dockerfiles étape par étape et attribue au résultat de chaque étape une image qui lui est propre. Ces images intermédiaires sont souvent appelées "calques". Si une commande dans un Dockerfile n'a pas changé, n'exécutera pas à nouveau cette étape lorsque vous recompilez le Dockerfile. À la place il réutilise la couche de la dernière création de l'image.
Auparavant, vous deviez faire des efforts pour ne pas recompiler libvpx à chaque fois
lors de la création de votre application. À la place, vous pouvez déplacer les instructions de compilation de libvpx
depuis votre build.sh
vers le Dockerfile
pour exploiter la mise en cache de Docker.
mécanisme:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen git && \
mkdir -p /opt/libvpx/build && \
git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
emconfigure ../src/configure --target=generic-gnu && \
emmake make
(en bref contenant tous les fichiers).
Notez que vous devez installer manuellement git et cloner libvpx, car vous n'avez pas
des montages liés lors de l'exécution de docker build
. Comme effet secondaire, il n'est pas nécessaire
maintenant.