Come puoi integrare WebAssembly in questa configurazione? In questo articolo, vedremo come utilizzare C/C++ e Emscripten come esempio.
WebAssembly (wasm) è spesso in frame come una primitiva per le prestazioni o un modo per eseguire il tuo C++ esistente il codebase sul web. Con squoosh.app, volevamo mostrare che c'è almeno un terzo punto di vista per wasm: sfruttare l'enorme ecosistemi di altri linguaggi di programmazione. Con Emscripten, puoi usare il codice C/C++, Rust ha il supporto wasm integrato e Go se ne sta occupando anche il team. Sono che molte altre lingue seguiranno.
In questi scenari, wasm non è il fulcro della tua app, ma un puzzle pezzi: un altro modulo. La tua app dispone già di asset JavaScript, CSS, immagine, basato sul web e magari anche un framework come React. In che modo integrare WebAssembly in questa configurazione? In questo articolo lavoreremo con C/C++ e Emscripten come esempio.
Docker
Ho trovato Docker preziosa quando lavoravo con Emscripten. C/C++ spesso sono scritte per funzionare con il sistema operativo su cui sono basate. È incredibilmente utile avere un ambiente coerente. Con Docker ottieni sistema Linux virtualizzato che è già configurato per funzionare con Emscripten e ha tutti gli strumenti e le dipendenze installati. Se manca qualcosa, puoi semplicemente installarlo senza doverti preoccupare delle conseguenze che può avere sul tuo computer o sul tuo altri progetti. In caso di problemi, scarta il container e avvia oltre. Se funziona una volta, puoi essere certo che continuerà a funzionare e producono risultati identici.
Il Docker Registry dispone di un parametro Emscripten immagine di trzeci che uso molto.
Integrazione con npm
Nella maggior parte dei casi, il punto di ingresso di un progetto web è il
package.json
. Per convenzione, la maggior parte dei progetti può essere creata con npm install &&
npm run build
.
In generale, gli artefatti di build prodotti da Emscripten (un .js
e un .wasm
) devono essere trattati solo come un altro modulo JavaScript e come
asset. Il file JavaScript può essere gestito da un bundler come webpack o rollup,
e il file wasm deve essere trattato come qualsiasi altro asset binario più grande,
in formato Docker.
Di conseguenza, gli artefatti di build di Emscripten devono essere creati prima dei tuoi "normali" il processo di compilazione si attiva:
{
"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 nuova attività build:emscripten
potrebbe richiamare Emscripten direttamente, ma
menzionate prima, consiglio di usare Docker per assicurarmi che l'ambiente di build sia
coerente.
docker run ... trzeci/emscripten ./build.sh
dice a Docker di avviare
utilizzando l'immagine trzeci/emscripten
ed esegui il comando ./build.sh
.
build.sh
è uno script shell che scriverai successivamente. --rm
dice
Docker per eliminare il container al termine dell'esecuzione. In questo modo,
e creare una raccolta di
immagini di macchine obsolete nel tempo. -v $(pwd):/src
significa che
vuoi che Docker esegua il "mirror" dalla directory corrente ($(pwd)
) a /src
all'interno
nel container. Eventuali modifiche apportate ai file nella directory /src
all'interno della
verrà rispecchiato nel progetto effettivo. Queste directory sottoposte a mirroring
sono chiamati "bind mounts".
Diamo un'occhiata a 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 "============================================="
C'è molto da analizzare qui!
set -e
inserisce la shell in "fail fast" . Se ci sono comandi nello script
restituiscono un errore, l'intero script viene interrotto immediatamente. Può essere
incredibilmente utile perché l'ultimo output dello script sarà sempre un successo
o l'errore che ha causato la mancata riuscita della build.
Con le istruzioni export
, definisci i valori di un paio di ambienti
come la codifica one-hot
delle variabili categoriche. Consentono di passare ulteriori parametri della riga di comando all'interfaccia
il compilatore (CFLAGS
), il compilatore C++ (CXXFLAGS
) e il linker (LDFLAGS
).
Tutti ricevono le impostazioni di ottimizzazione tramite OPTIMIZE
per assicurarsi che
tutto viene ottimizzato allo stesso modo. Esistono un paio di valori possibili
per la variabile OPTIMIZE
:
-O0
: non effettuare alcuna ottimizzazione. Il codice obsoleto non viene eliminato e Emscripten non minimizza il codice JavaScript che emette. Ideale per il debug.-O3
: ottimizza in modo aggressivo per il rendimento.-Os
: ottimizza in modo aggressivo per rendimento e dimensioni come secondario criterio.-Oz
: ottimizza in modo aggressivo per le dimensioni, sacrificando il rendimento se necessario.
Consiglio principalmente -Os
per il web.
Il comando emcc
ha una miriade di opzioni proprie. Tieni presente che il campo emcc è
dovrebbe essere una "sostituzione drop-in per compilatori come GCC o clang". Quindi tutti
i flag che potresti conoscere in GCC saranno probabilmente implementati da emcc come
beh. Il flag -s
è speciale in quanto consente di configurare Emscripten
in particolare. Tutte le opzioni disponibili sono disponibili nel pannello
settings.js
,
ma quel file può essere piuttosto complesso. Ecco un elenco delle segnalazioni Emscripten
che credo siano più importanti per gli sviluppatori web:
--bind
attiva embind.-s STRICT=1
interromperà il supporto per tutte le opzioni di build deprecate. Ciò garantisce che il tuo codice crea in modo compatibile con il futuro.-s ALLOW_MEMORY_GROWTH=1
consente di aumentare automaticamente la memoria se necessaria. Al momento della scrittura, Emscripten allocherà 16 MB di memoria all'inizio. Quando il codice alloca blocchi di memoria, questa opzione stabilisce se queste operazioni faranno in modo che l'intero modulo wasm non funzioni quando la memoria esauriti o se il glue code può espandere la memoria totale gestire l'allocazione.-s MALLOC=...
sceglie l'implementazionemalloc()
da utilizzare.emmalloc
è una piccola e rapida implementazione dimalloc()
specifica per Emscripten. La èdlmalloc
, un'implementazionemalloc()
completa. Devi eseguire passare adlmalloc
se stai allocando molti oggetti di piccole dimensioni spesso o se vuoi usare l'organizzazione in thread.-s EXPORT_ES6=1
trasformerà il codice JavaScript in un modulo ES6 con un un'esportazione predefinita che funziona con qualsiasi bundler. Richiede anche-s MODULARIZE=1
per da impostare.
I seguenti flag non sono sempre necessari o sono utili solo per il debug scopi:
-s FILESYSTEM=0
è un flag relativo a Emscripten e alla sua capacità di di emulare un file system quando il codice C/C++ utilizza operazioni di file system. Esegue alcune analisi sul codice che compila per decidere se includere dell'emulazione del file system o meno nel glue code. A volte, però, l'analisi può sbagliare e paghi 70 kB al massimo di collante per un'emulazione di file system che potrebbe non essere necessaria. Con-s FILESYSTEM=0
puoi forzare Emscripten a non includere questo codice.-g4
consentirà a Emscripten di includere le informazioni di debug in.wasm
e emette anche un file di mappe di origine per il modulo wasm. Puoi scoprire di più su il debug con Emscripten nel suo debugging .
Ecco fatto! Per testare questa configurazione, creiamo un piccolo 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);
}
E 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>
(Ecco un sintesi contenente tutti i file.
Per creare tutto, esegui
$ npm install
$ npm run build
$ npm run serve
Se passi a localhost:8080 dovresti visualizzare il seguente output nella Console DevTools:
Aggiunta di codice C/C++ come dipendenza
Se vuoi creare una libreria C/C++ per la tua applicazione web, è necessario che il suo codice
parte del tuo progetto. Puoi aggiungere il codice manualmente al repository del progetto
Oppure puoi usare npm per gestire anche questo tipo di dipendenze. Immaginiamo
voglio usare libvpx nella mia app web. libvpx
è una libreria C++ per codificare le immagini con VP8, il codec utilizzato nei file .webm
.
Tuttavia, libvpx non è su npm e non ha un package.json
, quindi non posso
lo installi direttamente usando npm.
Per uscire da questo rompicapo, c'è
napa. napa ti consente di installare qualsiasi Git
l'URL del repository come dipendenza nella cartella node_modules
.
Installa napa come dipendenza:
$ npm install --save napa
e assicurati di eseguire napa
come script di installazione:
{
// ...
"scripts": {
"install": "napa",
// ...
},
"napa": {
"libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}
Quando esegui npm install
, napa si occupa di clonare GitHub libvpx
repository in node_modules
con il nome libvpx
.
Ora puoi estendere lo script di build per creare libvpx. libvpx utilizza configure
e make
. Fortunatamente, Emscripten può contribuire a garantire che configure
e
make
usano il compilatore di Emscripten. A questo scopo ci sono i wrapper
Comandi emconfigure
e 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 ...
Una libreria C/C++ è divisa in due parti: le intestazioni (tradizionalmente .h
o
.hpp
) che definiscono le strutture di dati, le classi, le costanti e così via che un
che espone e la libreria effettiva (tradizionalmente i file .so
o .a
). A
usare la costante VPX_CODEC_ABI_VERSION
della libreria nel codice, devi
per includere i file di intestazione della libreria utilizzando un'istruzione #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;
}
Il problema è che il compilatore non sa dove cercare vpxenc.h
.
Questo è lo scopo del flag -I
. Indica al compilatore quali directory
per controllare i file di intestazione. Inoltre, devi fornire al compilatore anche
file della raccolta effettivo:
# ... 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 ...
Se esegui npm run build
ora, vedrai che il processo crea un nuovo .js
e un nuovo file .wasm
e che la pagina demo restituirà effettivamente la costante:
Noterai anche che il processo di compilazione richiede molto tempo. Il motivo per cui
tempi di creazione lunghi possono variare. Nel caso di libvpx, ci vuole molto tempo perché
compila un encoder e un decoder sia per VP8 che per VP9 ogni volta che
il comando build, anche se i file di origine non sono stati modificati. Anche un piccolo
la modifica al tuo my-module.cpp
richiederà molto tempo. Sarebbe molto
utile per mantenere gli artefatti di build di libvpx una volta che sono stati
ha costruito per la prima volta.
Un modo per farlo è utilizzare le variabili di ambiente.
# ... 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 ...
Ecco un riepilogo contenente tutti i file.
Il comando eval
ci consente di impostare le variabili di ambiente trasmettendo parametri
allo script di build. Il comando test
ignorerà la creazione di libvpx se
$SKIP_LIBVPX
impostato (su qualsiasi valore).
Ora puoi compilare il modulo ma saltando la nuova creazione di libvpx:
$ npm run build:emscripten -- SKIP_LIBVPX=1
Personalizzazione dell'ambiente di build
A volte le librerie dipendono da strumenti aggiuntivi per la loro creazione. Se queste dipendenze
mancanti nell'ambiente di build fornito dall'immagine Docker, devi
aggiungerli personalmente. Ad esempio, supponiamo che tu voglia creare anche
documentazione di libvpx utilizzando doxygen. Il dossigeno non è
disponibile all'interno del container Docker, ma puoi installarlo usando apt
.
Se lo fai in build.sh
, dovrai scaricare e reinstallare l'app
ogni volta che vuoi creare la tua raccolta. Non solo sarebbe
uno spreco di risorse, ma potrebbe impedirti di lavorare al progetto mentre sei offline.
In questo caso, ha senso creare la tua immagine Docker. Le immagini Docker vengono create
scrivendo un Dockerfile
che descriva i passaggi di build. I Dockerfile sono abbastanza
potente e ha molte
comandi, ma la maggior parte dei
che puoi usare usando solo FROM
, RUN
e ADD
. In questo caso:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
Con FROM
, puoi dichiarare quale immagine Docker utilizzare come punto di partenza
punto di accesso. Ho scelto trzeci/emscripten
come base, l'immagine che stai utilizzando
fin dall'inizio. Con RUN
, chiedi a Docker di eseguire i comandi shell all'interno
containerizzato. Le modifiche apportate da questi comandi al container ora fanno parte
l'immagine Docker. per assicurarti che l'immagine Docker sia stata creata
disponibili prima di eseguire build.sh
, devi modificare 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",
// ...
},
// ...
}
Ecco un riepilogo contenente tutti i file.
In questo modo verrà creata la tua immagine Docker, ma solo se non è ancora stata creata. Poi
tutto viene eseguito come prima, ma ora l'ambiente di build ha doxygen
il comando gcloud, che farà sì che la documentazione di libvpx venga generata come
beh.
Conclusione
Non sorprende che il codice C/C++ e npm non siano ideali, ma è possibile farla funzionare senza problemi con alcuni strumenti aggiuntivi e forniti da Docker. Questa configurazione non funziona per tutti i progetti, ma è comunque un punto di partenza utile da adattare alle tue esigenze. Se disponi miglioramenti, condividili.
Appendice: Utilizzo dei livelli di immagine Docker
Una soluzione alternativa è quella di incapsulare più di questi problemi con Docker l'approccio intelligente di Docker alla memorizzazione nella cache. Docker esegue i Dockerfile passo passo assegna al risultato di ogni passaggio una propria immagine. Queste immagini intermedie sono spesso chiamati "livelli". Se un comando in un Dockerfile non è cambiato, Docker non rieseguirà quel passaggio durante la creazione del Dockerfile. Invece riutilizza il livello dall'ultima creazione dell'immagine.
In precedenza, dovevi fare qualche sforzo per non ricreare ogni volta libvpx
a creare la tua app. Puoi invece spostare le istruzioni per la creazione di libvpx
da build.sh
a Dockerfile
per utilizzare la memorizzazione nella cache di Docker
meccanismo di attenzione:
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
Ecco un riepilogo contenente tutti i file.
Tieni presente che dovrai installare manualmente git e clonare libvpx poiché non disponi
eseguire il binding dei montaggi durante l'esecuzione di docker build
. Di conseguenza, non c'è bisogno di
napa.