Iscrizione di una libreria C a Wasm

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 del tuo sistema operativo 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:

Screenshot della console DevTools che mostra la versione corretta
numero.

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:

Screenshot della console DevTools che mostra un errore.

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!

dal riquadro di rete di DevTools e dall&#39;immagine generata.

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 procedi nel seguente modo:

<!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. Invio WebAssembly.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 un maximum , 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, quindi STACKTOP = 0.