In Che cos'è WebAssembly e da dove proviene?, Ho spiegato come siamo arrivati all'attuale WebAssembly. In questo articolo ti mostrerò il mio approccio alla compilazione di un programma C esistente, mkbitmap
, in WebAssembly. È più complesso dell'esempio hello world, in quanto include l'utilizzo di file, la comunicazione tra WebAssembly e JavaScript e il disegno su un canvas, ma è comunque abbastanza gestibile da non sopraffarti.
L'articolo è scritto per gli sviluppatori web che vogliono imparare WebAssembly e mostra passo dopo passo come procedere se volessi compilare qualcosa come mkbitmap
in WebAssembly. Come avviso, è del tutto normale che un'app o una libreria non venga compilata al primo tentativo, motivo per cui alcuni dei passaggi descritti di seguito non hanno funzionato, quindi ho dovuto tornare indietro e riprovare in modo diverso. L'articolo non mostra il comando magico di compilazione finale come se fosse caduto dal cielo, ma descrive i miei progressi reali, incluse alcune frustrazioni.
Informazioni su mkbitmap
Il programma C mkbitmap
legge un'immagine e vi applica una o più delle seguenti operazioni, in questo ordine: inversione, filtro passa alto, ridimensionamento e soglia. Ogni operazione può essere controllata e attivata o disattivata singolarmente. L'uso principale di mkbitmap
è convertire le immagini a colori o in scala di grigi in un formato adatto come input per altri programmi, in particolare il programma di tracciamento potrace
che costituisce la base di SVGcode. Come strumento di pre-elaborazione, mkbitmap
è particolarmente utile per convertire disegni al tratto scansionati, come fumetti o testo scritto a mano, in immagini in bianco e nero ad alta risoluzione.
Utilizzi mkbitmap
passando un numero di opzioni e uno o più nomi di file. Per tutti i dettagli, consulta la pagina del manuale dello strumento:
$ mkbitmap [options] [filename...]


mkbitmap -f 2 -s 2 -t 0.48
(fonte).Ottieni il codice
Il primo passaggio consiste nell'ottenere il codice sorgente di mkbitmap
. Puoi trovarlo sul sito web del progetto. Al momento della stesura di questo documento, potrace-1.16.tar.gz è l'ultima versione.
Compilare e installare localmente
Il passaggio successivo consiste nel compilare e installare lo strumento localmente per capire come si comporta. Il file INSTALL
contiene le seguenti istruzioni:
cd
alla directory contenente il codice sorgente del pacchetto e digita./configure
per configurare il pacchetto per il tuo sistema.L'esecuzione di
configure
potrebbe richiedere un po' di tempo. Durante l'esecuzione, stampa alcuni messaggi che indicano le funzionalità che sta controllando.Digita
make
per compilare il pacchetto.(Facoltativo) Digita
make check
per eseguire tutti gli autotest inclusi nel pacchetto, in genere utilizzando i file binari non installati appena creati.Digita
make install
per installare i programmi e tutti i file di dati e la documentazione. Quando l'installazione viene eseguita in un prefisso di proprietà di root, è consigliabile configurare e creare il pacchetto come utente normale ed eseguire solo la fasemake install
con privilegi di root.
Se segui questi passaggi, dovresti ottenere due eseguibili, potrace
e mkbitmap
. Quest'ultimo è l'argomento principale di questo articolo. Puoi verificare che abbia funzionato correttamente eseguendo mkbitmap --version
. Ecco l'output di tutti e quattro i passaggi della mia macchina, molto ridotto per brevità:
Passaggio 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
Passaggio 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'.
Passaggio 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'.
Passaggio 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'.
Per verificare se l'operazione è riuscita, esegui mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Se ricevi i dettagli della versione, hai compilato e installato correttamente mkbitmap
. Successivamente, fai in modo che l'equivalente di questi passaggi funzioni con WebAssembly.
Compilare mkbitmap
in WebAssembly
Emscripten è uno strumento per compilare programmi C/C++ in WebAssembly. La documentazione Building Projects di Emscripten afferma quanto segue:
Creare progetti di grandi dimensioni con Emscripten è molto semplice. Emscripten fornisce due semplici script che configurano i makefile per utilizzare
emcc
come sostituzione diretta digcc
. Nella maggior parte dei casi, il resto del sistema di compilazione corrente del progetto rimane invariato.
La documentazione continua (con qualche modifica per brevità):
Considera il caso in cui normalmente esegui la build con i seguenti comandi:
./configure
make
Per eseguire la build con Emscripten, devi invece utilizzare i seguenti comandi:
emconfigure ./configure
emmake make
Quindi, essenzialmente ./configure
diventa emconfigure ./configure
e make
diventa emmake make
. Di seguito viene mostrato come farlo con mkbitmap
.
Passaggio 0, make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
Passaggio 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
Passaggio 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'.
Se tutto è andato bene, ora dovrebbero esserci file .wasm
da qualche parte nella directory. Puoi trovarli eseguendo find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
Gli ultimi due sembrano promettenti, quindi cd
nella directory src/
. Ora sono disponibili anche due nuovi file corrispondenti, mkbitmap
e potrace
. Per questo articolo, è pertinente solo mkbitmap
. Il fatto che non abbiano l'estensione .js
è un po' fuorviante, ma in realtà sono file JavaScript, verificabili con una rapida chiamata head
:
$ 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)
Rinomina il file JavaScript in mkbitmap.js
chiamando mv mkbitmap mkbitmap.js
(e mv potrace potrace.js
, se vuoi).
Ora è il momento del primo test per verificare se ha funzionato eseguendo il file con Node.js dalla riga di comando eseguendo node mkbitmap.js --version
:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Hai compilato correttamente mkbitmap
in WebAssembly. Il passaggio successivo consiste nel farlo funzionare nel browser.
mkbitmap
con WebAssembly nel browser
Copia i file mkbitmap.js
e mkbitmap.wasm
in una nuova directory denominata mkbitmap
e crea un file boilerplate HTML index.html
che carichi il file 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>
Avvia un server locale che gestisce la directory mkbitmap
e aprirla nel browser. Dovresti visualizzare un prompt che ti chiede di inserire un input. Questo comportamento è previsto, poiché, secondo la pagina man dello strumento, "[i]f no filename arguments are given, then mkbitmap acts as a filter, reading from standard input", che per Emscripten per impostazione predefinita è un prompt()
.
Impedire l'esecuzione automatica
Per impedire l'esecuzione immediata di mkbitmap
e fare in modo che attenda l'input dell'utente, devi comprendere l'oggetto Module
di Emscripten. Module
è un oggetto JavaScript globale con attributi che il codice generato da Emscripten chiama in vari punti della sua esecuzione.
Puoi fornire un'implementazione di Module
per controllare l'esecuzione del codice.
Quando un'applicazione Emscripten viene avviata, esamina i valori dell'oggetto Module
e li applica.
Nel caso di mkbitmap
, imposta Module.noInitialRun
su true
per impedire l'esecuzione iniziale che ha causato la visualizzazione del prompt. Crea uno script chiamato script.js
, includilo prima di <script src="mkbitmap.js"></script>
in index.html
e aggiungi il seguente codice a script.js
. Quando ricarichi l'app, il prompt non dovrebbe più essere visualizzato.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Crea una build modulare con altri flag di build
Per fornire input all'app, puoi utilizzare il supporto del file system di Emscripten in Module.FS
. La sezione Inclusione del supporto del file system della documentazione afferma:
Emscripten decide se includere automaticamente il supporto del file system. Molti programmi non hanno bisogno di file e il supporto del file system non è trascurabile in termini di dimensioni, quindi Emscripten evita di includerlo quando non ne vede il motivo. Ciò significa che se il codice C/C++ non accede ai file, l'oggetto
FS
e altre API del file system non verranno inclusi nell'output. D'altra parte, se il tuo codice C/C++ utilizza file, il supporto del file system verrà incluso automaticamente.
Purtroppo mkbitmap
è uno dei casi in cui Emscripten non include automaticamente il supporto del file system, quindi devi indicarglielo esplicitamente. Ciò significa che devi seguire i passaggi emconfigure
e emmake
descritti in precedenza, con un paio di flag aggiuntivi impostati tramite un argomento CFLAGS
. I seguenti flag potrebbero essere utili anche per altri progetti.
- Imposta
-sFILESYSTEM=1
in modo che sia incluso il supporto del file system. - Imposta
-sEXPORTED_RUNTIME_METHODS=FS,callMain
in modo che vengano esportatiModule.FS
eModule.callMain
. - Imposta
-sMODULARIZE=1
e-sEXPORT_ES6
per generare un modulo ES6 moderno. - Imposta
-sINVOKE_RUN=0
per impedire l'esecuzione iniziale che ha causato la visualizzazione del prompt.
In questo caso specifico, devi impostare il flag --host
su wasm32
per indicare allo script configure
che stai eseguendo la compilazione per WebAssembly.
Il comando emconfigure
finale ha il seguente aspetto:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
Non dimenticare di eseguire di nuovo emmake make
e copiare i file appena creati nella cartella mkbitmap
.
Modifica index.html
in modo che carichi solo il modulo ES script.js
, da cui importi poi il modulo 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();
Quando apri l'app nel browser, dovresti vedere l'oggetto Module
registrato nella console DevTools e il prompt non è più presente, poiché la funzione main()
di mkbitmap
non viene più chiamata all'inizio.
Esegui manualmente la funzione principale
Il passaggio successivo consiste nel chiamare manualmente la funzione main()
di mkbitmap
eseguendo Module.callMain()
. La funzione callMain()
accetta un array di argomenti, che corrispondono uno per uno a quelli che passeresti dalla riga di comando. Se nella riga di comando esegui mkbitmap -v
, nel browser devi chiamare Module.callMain(['-v'])
. In questo modo, il numero di versione di mkbitmap
viene registrato nella console DevTools.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
Reindirizzare l'output standard
L'output standard (stdout
) per impostazione predefinita è la console. Tuttavia, puoi reindirizzarlo a qualcos'altro, ad esempio una funzione che memorizza l'output in una variabile. Ciò significa che puoi aggiungere l'output all'HTML impostando la proprietà 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();
Inserire il file di input nel file system in memoria
Per inserire il file di input nel file system della memoria, devi avere l'equivalente di mkbitmap filename
nella riga di comando. Per capire il mio approccio, vediamo prima come mkbitmap
si aspetta l'input e crea l'output.
I formati di input supportati di mkbitmap
sono PNM (PBM, PGM, PPM) e BMP. I formati di output sono PBM per le bitmap e PGM per le graymap. Se viene fornito un argomento filename
, mkbitmap
creerà per impostazione predefinita un file di output il cui nome viene ottenuto dal nome del file di input modificando il suffisso in .pbm
. Ad esempio, per il nome del file di input example.bmp
, il nome del file di output sarebbe example.pbm
.
Emscripten fornisce un file system virtuale che simula il file system locale, in modo che il codice nativo che utilizza le API di file sincrone possa essere compilato ed eseguito con poche o nessuna modifica.
Per fare in modo che mkbitmap
legga un file di input come se fosse stato passato come argomento della riga di comando filename
, devi utilizzare l'oggetto FS
fornito da Emscripten.
L'oggetto FS
è supportato da un file system in memoria (comunemente chiamato MEMFS) e ha una funzione writeFile()
che utilizzi per scrivere file nel file system virtuale. Utilizza writeFile()
come mostrato nel seguente esempio di codice.
Per verificare che l'operazione di scrittura del file sia andata a buon fine, esegui la funzione readdir()
dell'oggetto FS
con il parametro '/'
. Vedrai example.bmp
e una serie di file predefiniti che vengono sempre creati automaticamente.
Tieni presente che la chiamata precedente a Module.callMain(['-v'])
per la stampa del numero di versione è stata rimossa. Ciò è dovuto al fatto che Module.callMain()
è una funzione che in genere prevede di essere eseguita una sola volta.
// 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();
Prima esecuzione effettiva
Con tutto a posto, esegui mkbitmap
eseguendo Module.callMain(['example.bmp'])
. Registra i contenuti della cartella '/'
di MEMFS. Dovresti vedere il file di output example.pbm
appena creato accanto al file di input 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();
Estrarre il file di output dal file system di memoria
La funzione readFile()
dell'oggetto FS
consente di estrarre example.pbm
creato nell'ultimo passaggio dal file system di memoria. La funzione restituisce un Uint8Array
che viene convertito in un oggetto File
e salvato su disco, poiché i browser in genere non supportano i file PBM per la visualizzazione diretta nel browser.
Esistono modi più eleganti per salvare un file, ma l'utilizzo di un <a download>
creato dinamicamente è il più ampiamente supportato. Una volta salvato il file, puoi aprirlo nel tuo visualizzatore di immagini preferito.
// 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();
Aggiungere un'interfaccia utente interattiva
A questo punto, il file di input è hardcoded e mkbitmap
viene eseguito con i parametri predefiniti. Il passaggio finale consiste nel consentire all'utente di selezionare dinamicamente un file di input, modificare i parametri mkbitmap
e quindi eseguire lo strumento con le opzioni selezionate.
// 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']);
Il formato immagine PBM non è particolarmente difficile da analizzare, quindi con un po' di codice JavaScript potresti persino mostrare un'anteprima dell'immagine di output. Per un modo per farlo, consulta il codice sorgente della demo incorporata riportata di seguito.
Conclusione
Congratulazioni, hai compilato correttamente mkbitmap
in WebAssembly e l'hai fatto funzionare nel browser. Ci sono stati alcuni vicoli ciechi e ho dovuto compilare lo strumento più di una volta prima che funzionasse, ma come ho scritto sopra, fa parte dell'esperienza. Se hai difficoltà, ricorda anche il tag webassembly
di Stack Overflow. Buona compilazione!
Ringraziamenti
Questo articolo è stato esaminato da Sam Clegg e Rachel Andrew.