A volte potresti voler utilizzare una libreria disponibile solo come codice C o C++. Tradizionalmente, è qui che si arrende. Beh, non più, perché ora abbiamo Emscripten e WebAssembly (o Wasm)!
La toolchain
Mi sono fissato l'obiettivo di capire come compilare del codice C esistente Wasm. Si è verificato un po' di rumore intorno al backend Wasm di LLVM, quindi Ho iniziato ad approfondire questo aspetto. Mentre puoi ottenere semplici programmi da compilare in questo modo, nel momento in cui vuoi usare la libreria standard di C o anche più file, probabilmente riscontrerai problemi. Questo mi ha portato alla grande alla lezione che ho imparato:
Nonostante Emscripten prima fosse un compilatore C-to-asm.js, da allora è diventato un compilatore target Wasm ed è durante il passaggio al backend LLVM ufficiale internamente. Emscripten offre anche Implementazione compatibile con le versioni precedenti della libreria standard di C. Usa Emscripten. it comporta molto lavoro nascosto, emula un file system, fornisce la gestione della memoria, esegue il wrapping di OpenGL con WebGL, molte cose che non ti serve sperimentare e sviluppare autonomamente.
Anche se potrebbe sembrare che devi preoccuparti per il gonfiore, sicuramente mi preoccupano il compilatore Emscripten rimuove tutto ciò che non è necessario. Nel mio esperimenti, i moduli Wasm risultanti sono dimensionati in modo appropriato per la logica e i team di Emscripten e WebAssembly stanno lavorando per rendere per ridurli in futuro.
Puoi ottenere Emscripten seguendo le istruzioni sul suo sito web o utilizzando Homebrew. Se sei un fan di come me e non voglio installare elementi sul tuo sistema di sperimentare con WebAssembly, c'è un'infrastruttura Immagine Docker che puoi utilizzare anziché:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
Compilare qualcosa di semplice
Prendiamo l'esempio quasi canonico della scrittura di una funzione in Do che calcola l'° numero di Fibonacci:
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if(n <= 0){
return 0;
}
int i, t, a = 0, b = 1;
for (i = 1; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
Se conosci il C, la funzione in sé non dovrebbe sorprenderti troppo. Anche se conoscete C ma JavaScript, dovreste riuscire a capire cosa succede qui.
emscripten.h
è un file di intestazione fornito da Emscripten. Ci serve solo perché
hanno accesso alla macro EMSCRIPTEN_KEEPALIVE
, ma
offre molte più funzionalità.
Questa macro indica al compilatore di non rimuovere una funzione anche se appare
inutilizzati. Se omettiamo la macro, il compilatore ottimizzerebbe la funzione
nessuno lo usa, dopotutto.
Salviamo tutto questo in un file chiamato fib.c
. Per convertirlo in un file .wasm
,
passare al comando di compilazione di Emscripten emcc
:
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c
Analizziamo questo comando. emcc
è il compilatore di Emscripten. fib.c
è la nostra C
. Stai andando bene. -s WASM=1
indica a Emscripten di fornirci un file Wasm
anziché un file asm.js.
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
indica al compilatore di uscire
Funzione cwrap()
disponibile nel file JavaScript — ulteriori informazioni su questa funzione
in un secondo momento. -O3
indica al compilatore di ottimizzare in modo aggressivo. Puoi scegliere valori inferiori
numeri per ridurre i tempi di build, ma ciò renderà anche i pacchetti risultanti
perché il compilatore potrebbe non rimuovere il codice inutilizzato.
Dopo aver eseguito il comando, dovresti visualizzare un file JavaScript chiamato
a.out.js
e un file WebAssembly denominato a.out.wasm
. Il file Wasm (o
"module") contiene il nostro codice C compilato e dovrebbe essere abbastanza piccolo. La
il file JavaScript si occupa di caricare e inizializzare il modulo Wasm e
offrendo un'API più efficiente. Se necessario, si occuperà anche di configurare
lo stack, l'heap e altre funzionalità che solitamente dovrebbero essere fornite
durante la scrittura del codice C. Di conseguenza, il file JavaScript è
più grande, con un peso di 19 KB (~5 KB per gzip).
Gestire qualcosa di semplice
Il modo più semplice per caricare ed eseguire il modulo è utilizzare il codice JavaScript generato
. Una volta caricato il file,
Module
globale
a tua disposizione. Utilizza le funzionalità di
cwrap
per creare una funzione nativa JavaScript che si occupi della conversione dei parametri
a qualcosa di C-friendly e richiamare la funzione con wrapping. cwrap
prende la
nome della funzione, tipo restituito e tipi di argomento come argomenti, in questo ordine:
<script src="a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('fib', 'number', ['number']);
console.log(fib(12));
};
</script>
Se esegui questo codice, dovresti vedere il comando "144" nella console, che è il dodicesimo numero di Fibonacci.
Santo Graal: compilazione di una biblioteca C
Finora, il codice C che abbiamo scritto era stato scritto tenendo conto di Wasm. Un nucleo per WebAssembly, tuttavia, è prendere l'ecosistema esistente di soluzioni librerie e per consentire agli sviluppatori di utilizzarle sul web. Queste librerie spesso si basano sulla libreria standard di C, su un sistema operativo, su un file system e su altre cose nuove. Emscripten offre la maggior parte di queste funzionalità, anche se ci sono alcune limitazioni.
Torniamo al mio obiettivo originale: compilare un codificatore per WebP a Wasm. La il codice sorgente WebP sia scritto in C e disponibile GitHub e alcune estensioni documentazione dell'API. È un ottimo punto di partenza.
$ git clone https://github.com/webmproject/libwebp
Per iniziare, proviamo a esporre WebPGetEncoderVersion()
encode.h
in JavaScript scrivendo un file C denominato webp.c
:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
Questo è un programma semplice e valido per verificare se siamo in grado di ottenere il codice sorgente di libwebp per la compilazione, in quanto non sono necessari parametri o strutture di dati complesse per richiamare questa funzione.
Per compilare questo programma, dobbiamo comunicare al compilatore dove può trovare
i file di intestazione di libwebp usando il flag -I
e passano anche tutti i file C di
libwebp di cui ha bisogno. Devo essere sincera: ho appena dato tutte le risposte
ho trovato i file e ho fatto affidamento sul compilatore per rimuovere tutto ciò che era
inutili. Sembrava funzionare benissimo!
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
Ora abbiamo solo bisogno di codice HTML e JavaScript per caricare il nostro nuovo modulo:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
Vedremo il numero di versione della correzione output:
Recuperare un'immagine da JavaScript a Wasm
Ottenere il numero di versione dell'encoder è ottimo e tutto, ma la codifica di una sarebbe più impressionante, vero? Procedo, allora.
La prima domanda a cui dobbiamo rispondere è: come facciamo a portare l'immagine nella terra di Wasm?
Esaminando
dell'API di codifica di libwebp, si aspetta
un array di byte in RGB, RGBA, BGR o BGRA. Fortunatamente, l'API Canvas
getImageData()
,
che ci offre
Uint8ClampedArray
contenenti i dati immagine in RGBA:
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then((resp) => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
Ora è "solo" copiare i dati da JavaScript a Wasm terra. Per questo, abbiamo bisogno di esporre due funzioni aggiuntive. che alloca per l'immagine all'interno della terra di Wasm e quella che la libera di nuovo:
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
create_buffer
alloca un buffer per l'immagine RGBA, quindi 4 byte per pixel.
Il puntatore restituito da malloc()
è l'indirizzo della prima cella di memoria di
del buffer. Quando il puntatore viene restituito alla pagina JavaScript, viene considerato
solo un numero. Dopo aver esposto la funzione a JavaScript utilizzando cwrap
, possiamo
useremo quel numero per trovare l'inizio del nostro buffer e copiare i dati dell'immagine.
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
Gran finale: codificare l'immagine
L'immagine è ora disponibile in Wasm Land. È il momento di chiamare il codificatore WebP
fai il suo lavoro! Esaminando
Documentazione WebP, WebPEncodeRGBA
sembra la soluzione perfetta. La funzione porta un puntatore all'immagine di input e
per le sue dimensioni, nonché un'opzione di qualità compresa tra 0 e 100. Inoltre, assegna
un buffer di output, che dobbiamo liberare usando WebPFree()
una volta
fatto con l'immagine WebP.
Il risultato dell'operazione di codifica è un buffer di output e la sua lunghezza. Poiché le funzioni in C non possono avere array come tipi restituiti (a meno che non allochiamo memoria in modo dinamico), ho fatto ricorso a un array globale statico. Non so nulla di C (in effetti, si basa sul fatto che i puntatori Wasm sono larghi 32 bit), ma per mantenere semplice, penso che sia una scorciatoia equa.
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
Ora con tutto questo a disposizione, possiamo chiamare la funzione di codifica, del puntatore e delle dimensioni dell'immagine, lo metteremo in un buffer JavaScript-land nostro, rilasciare tutti i buffer di Wasm-land che abbiamo assegnato nel processo.
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
A seconda delle dimensioni dell'immagine, potrebbe verificarsi un errore in cui Wasm non può aumentare la memoria a sufficienza per contenere sia l'immagine di input che quella di output:
Fortunatamente, la soluzione a questo problema si trova nel messaggio di errore. Dobbiamo solo
aggiungi -s ALLOW_MEMORY_GROWTH=1
al nostro comando di compilazione.
Ecco fatto! Abbiamo compilato un codificatore WebP e transcodificato un'immagine JPEG in
WebP Per dimostrare che ha funzionato, possiamo trasformare il buffer dei risultati in un blob e utilizzare
su un elemento <img>
:
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);
Ecco la gloria di una nuova immagine WebP!
Conclusione
Non è una passeggiata per far funzionare una libreria C nel browser, ma una volta il processo complessivo e il funzionamento del flusso di dati, diventa più facile e i risultati possono essere sbalorditivi.
WebAssembly apre molte nuove possibilità sul web per l'elaborazione, e giocare. Ricorda che Wasm non è una soluzione miracolosa che dovrebbe essere applicato a tutto, ma quando si incontra uno di questi colli di bottiglia, Wasm può essere uno strumento incredibilmente utile.
Contenuti bonus: gestire qualcosa di semplice nel modo più difficile
Se vuoi provare a evitare il file JavaScript generato, potresti riuscire a. Torniamo all'esempio di Fibonacci. Per caricarla ed eseguirla da soli, possiamo segui questi passaggi:
<!DOCTYPE html>
<script>
(async function () {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
STACKTOP: 0,
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/a.out.wasm'),
imports,
);
console.log(instance.exports._fib(12));
})();
</script>
I moduli WebAssembly creati da Emscripten non hanno memoria per funzionare
a meno che tu non fornisca loro memoria. Il modo in cui fornisci un modulo Wasm
qualsiasi cosa consiste nell'utilizzare l'oggetto imports
, il secondo parametro del
instantiateStreaming
. Il modulo Wasm può accedere a tutto ciò che si trova
dell'oggetto di importazione, ma nient'altro al suo esterno. Per convenzione, i moduli
compilati da Emscripting prevedono un paio di cose dal caricamento
questo ambiente:
- Il primo è
env.memory
. Il modulo Wasm è inconsapevole delle mondo per così dire, quindi serve un po' di memoria con cui lavorare. InvioWebAssembly.Memory
Rappresenta una porzione di memoria lineare (facoltativamente crescere). Le taglie sono espressi in "in unità di pagine WebAssembly", ovvero il codice riportato sopra assegna 1 pagina di memoria e ogni pagina ha una dimensione di 64 KiB. Senza fornire unmaximum
, la crescita della memoria teoricamente è illimitata (Chrome al momento un limite fisso di 2 GB). Per la maggior parte dei moduli WebAssembly non dovrebbe essere necessario impostare un massimo. env.STACKTOP
definisce la posizione in cui dovrebbe iniziare la crescita dello stack. Stack per effettuare chiamate di funzione e allocare la memoria per le variabili locali. Poiché non svolgiamo attività di gestione dinamica della memoria nei di Fibonacci, possiamo usare l'intera memoria come stack, quindiSTACKTOP = 0
.