Compilazione di mkbitmap in WebAssembly

In Che cos'è WebAssembly e da dove viene?, Ho spiegato come siamo finiti con il WebAssembly di oggi. 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 lavoro con i file, comunicazione tra WebAssembly e destinazione JavaScript e disegno su una tela, ma è comunque abbastanza gestibile da non sovraccaricarti.

L'articolo è stato scritto per gli sviluppatori web che vogliono imparare a utilizzare WebAssembly e mostra passo passo come procedere se volessi compilare qualcosa come mkbitmap in WebAssembly. Ti avviso che non riuscire a compilare un'app o una libreria alla prima esecuzione è del tutto normale, 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 magico comando di compilazione finale come se fosse caduto dal cielo, ma descrive piuttosto i miei progressi effettivi, incluse alcune frustrazioni.

Informazioni su mkbitmap

Il programma C di mkbitmap legge un'immagine e vi applica una o più delle seguenti operazioni, in questo ordine: inversione, filtro passa-alto, scalabilità e soglia. Ogni operazione può essere controllata e attivata o disattivata individualmente. L'utilizzo principale di mkbitmap consiste nella conversione di 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 è alla base del SVGcode. Come strumento di pre-elaborazione, mkbitmap è particolarmente utile per la conversione di disegni al disegno scansionati, come cartoni animati o testo scritto a mano, in immagini bilivello ad alta risoluzione.

Puoi usare mkbitmap trasmettendo una serie di opzioni e uno o più nomi di file. Per tutti i dettagli, consulta la pagina man dello strumento:

$ mkbitmap [options] [filename...]
Immagine cartone animato a colori.
L'immagine originale (Fonte).
Immagine fumetto convertita in scala di grigi dopo la pre-elaborazione.
Prima scala, poi soglia: mkbitmap -f 2 -s 2 -t 0.48 (Origine).

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 articolo, potrace-1.16.tar.gz è la versione più recente.

Compila e installa localmente

Il passaggio successivo consiste nella compilazione e nell'installazione dello strumento in locale per avere un'idea di come si comporta. Il file INSTALL contiene le seguenti istruzioni:

  1. 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. Mentre è in esecuzione, stampa alcuni messaggi per indicare quali funzionalità sta controllando.

  2. Digita make per compilare il pacchetto.

  3. Facoltativamente, digita make check per eseguire eventuali test automatici forniti con il pacchetto, in genere utilizzando i programmi binari disinstallati appena creati.

  4. Digita make install per installare i programmi, nonché eventuali file di dati e documentazione. Durante l'installazione in un prefisso di proprietà del root, è consigliabile configurare il pacchetto e crearlo come utente normale e utilizzare solo la fase make install con privilegi root.

Seguendo questi passaggi, dovresti ottenere due eseguibili, potrace e mkbitmap, il secondo è 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 tagliato 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 ha funzionato, esegui mkbitmap --version:

$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

Se visualizzi i dettagli sulla versione, significa che hai compilato e installato mkbitmap. Quindi, fai funzionare l'equivalente di questi passaggi con WebAssembly.

Compila mkbitmap in WebAssembly

Emscripten è uno strumento per compilare programmi C/C++ in WebAssembly. La documentazione di Emscripten relativa ai Progetti di edifici illustra quanto segue:

Creare grandi progetti con Emscripten è molto facile. Emscripten fornisce due semplici script che configurano i makefile in modo che utilizzino emcc come sostituzione diretta di gcc. Nella maggior parte dei casi, il resto del sistema di compilazione attuale del progetto rimane invariato.

La documentazione continua (poco modificata per brevità):

Considera il caso in cui normalmente crei con i seguenti comandi:

./configure
make

Per creare con Emscripten, utilizza invece questi comandi:

emconfigure ./configure
emmake make

Di conseguenza, in pratica ./configure diventa emconfigure ./configure e make diventa emmake make. Di seguito viene mostrato come eseguire questa operazione 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 a posto, ora dovrebbero esserci .wasm file nella directory. Per trovarli, esegui find . -name "*.wasm":

$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm

Le ultime due sembrano promettenti, quindi cd nella directory src/. Ora ci sono anche due nuovi file corrispondenti, mkbitmap e potrace. Per questo articolo, è pertinente solo mkbitmap. Il fatto che non abbiano l'estensione .js crea un po' di confusione, 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 rispettivamente se vuoi). Ora è il momento del primo test per vedere se ha funzionato eseguendo il file con Node.js sulla 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 passo successivo è far funzionare questa app 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 gestisca la directory mkbitmap e aprilo nel browser. Dovresti visualizzare un messaggio che ti chiede di inserire i dati. Questo è come previsto perché, secondo la pagina di manuale dello strumento, "[i]f non vengono forniti argomenti relativi ai nomi file, quindi mkbitmap agisce come filtro, leggendo dall'input standard", che per Emscripten per impostazione predefinita è una prompt().

L&#39;app mkbitmap che mostra un prompt che richiede l&#39;input.

Impedisci l'esecuzione automatica

Per interrompere immediatamente l'esecuzione di mkbitmap e farla attendere invece 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. All'avvio di un'applicazione Emscripten, vengono esaminati i valori dell'oggetto Module e applicati.

Nel caso di mkbitmap, imposta Module.noInitialRun su true per impedire l'esecuzione iniziale che ha causato la visualizzazione del prompt. Crea uno script denominato script.js, includilo prima del <script src="mkbitmap.js"></script> in index.html e aggiungi il seguente codice a script.js. Quando ricarichi l'app, la richiesta dovrebbe scomparire.

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 per il file system nella documentazione indica:

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 di dimensioni, quindi Emscripten evita di includerlo quando non ne vede un motivo. Ciò significa che se il tuo codice C/C++ non accede ai file, l'oggetto FS e altre API del file system non saranno inclusi nell'output. E, d'altra parte, se il codice C/C++ utilizza file, il supporto per il file system sarà incluso automaticamente.

Purtroppo mkbitmap è uno dei casi in cui Emscripten non include automaticamente il supporto del file system, quindi devi dirglielo 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 possono essere utili anche per altri progetti.

Inoltre, in questo caso particolare, devi impostare il flag --host su wasm32 per indicare allo script configure che stai compilando 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 nuovamente 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 potrai importare 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 la richiesta non è più disponibile, poiché la funzione main() di mkbitmap non viene più chiamata all'inizio.

L&#39;app mkbitmap con una schermata bianca che mostra l&#39;oggetto Modulo registrato nella console DevTools.

Eseguire 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 alla volta a quello che passeresti sulla riga di comando. Se sulla riga di comando devi eseguire mkbitmap -v, devi chiamare Module.callMain(['-v']) nel browser. 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();

L&#39;app mkbitmap con una schermata bianca che mostra il numero di versione mkbitmap registrato nella console DevTools.

Reindirizza l'output standard

Per impostazione predefinita, l'output standard (stdout) è la console. Tuttavia, puoi reindirizzarla a qualcos'altro, ad esempio una funzione che archivia l'output in una variabile. Ciò significa che puoi aggiungere l'output al codice 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();

L&#39;app mkbitmap che mostra il numero di versione mkbitmap.

Recupera il file di input nel file system di memoria

Per inserire il file di input nel file system di memoria, è necessario l'equivalente di mkbitmap filename nella riga di comando. Per capire l'approccio da adottare, ecco alcune informazioni generali su 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 mappe bitmap e PGM per le mappe grigie. Se viene fornito un argomento filename, per impostazione predefinita mkbitmap creerà un file di output il cui nome è 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 sarà 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 modifiche o nessuna modifica. Per consentire a mkbitmap di leggere 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 indicato come MEMFS) e dispone di una funzione writeFile() che puoi utilizzare per scrivere file nel file system virtuale. Utilizzi writeFile() come mostrato nel seguente esempio di codice.

Per verificare che l'operazione di scrittura del file abbia funzionato, 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();

L&#39;app mkbitmap che mostra un array di file nel file system di memoria, tra cui example.bmp.

Prima esecuzione effettiva

Una volta configurata, 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();

L&#39;app mkbitmap che mostra un array di file nel file system di memoria, tra cui example.bmp ed example.pbm.

Recupera il file di output dal file system di memoria

La funzione readFile() dell'oggetto FS consente di recuperare il valore example.pbm creato nell'ultimo passaggio dal file system di memoria. La funzione restituisce un Uint8Array che converti in oggetto File e salva su disco, poiché i browser in genere non supportano i file PBM per la visualizzazione diretta nel browser. Esistono dei modi più eleganti per salvare un file, ma l'utilizzo di un <a download> creato dinamicamente è quello più 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();

Finder di macOS con un&#39;anteprima del file .bmp di input e del file .pbm di output.

Aggiungi un'interfaccia utente interattiva

A questo punto, il file di input è impostato come hardcoded e mkbitmap viene eseguito con i parametri predefiniti. Il passaggio finale consiste nel consentire all'utente di selezionare in modo dinamico un file di input, modificare i parametri mkbitmap ed 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']);

Poiché il formato dell'immagine PBM non è particolarmente difficile da analizzare, con un po' di codice JavaScript potresti persino mostrare un'anteprima dell'immagine di output. Per sapere come procedere, consulta il codice sorgente della demo incorporata di seguito.

Conclusione

Congratulazioni, hai compilato correttamente mkbitmap in WebAssembly e lo hai fatto funzionare nel browser. C'erano delle strade senza uscita e hai dovuto compilare lo strumento più di una volta finché non ha funzionato, ma, come ho scritto sopra, questo fa parte dell'esperienza. In caso di problemi, ricorda anche il tag webassembly di StackOverflow. Buona compilazione!

Ringraziamenti

Questo articolo è stato recensito da Sam Clegg e Rachel Andrew.