C-Bibliothek in Wasm einfügen

Manchmal möchten Sie eine Bibliothek verwenden, die nur als C- oder C++-Code verfügbar ist. Traditionell gibt man hier auf. Nun, nicht mehr, denn jetzt haben wir Emscripten und WebAssembly (oder Wasm)!

Die Toolchain

Ich habe es mir zum Ziel gesetzt, einen vorhandenen C-Code zu kompilieren, Wasm. Um das Wasm-Back-End von LLVM herum gab es Unstimmigkeiten, Ich fing an, mich damit zu beschäftigen. Während können Sie einfache Programme Wenn Sie dann die Standardbibliothek des C verwenden oder sogar mehrere Dateien enthält, werden Sie wahrscheinlich auf Probleme stoßen. So kam ich zum wichtigsten Lektion, die ich gelernt habe:

Während Emscripten früher als C-to-asm.js-Compiler fungierte, ist es inzwischen zu einem auf Wasm ausrichten und ist beim Wechsel zum offiziellen LLVM-Back-End. Emscripten bietet außerdem eine Wasm-kompatible Implementierung der C-Standardbibliothek. Emscripten verwenden Es verborgene Arbeit mit sich bringt, emuliert ein Dateisystem, bietet Speicherverwaltung, umschließt OpenGL mit WebGL – einer die Sie nicht selbst entwickeln müssen.

Das klingt zwar so, als müsstest du dir Sorgen um Blähungen machen – ich mache mir schon Sorgen, – Der Emscripten-Compiler entfernt alles, was nicht benötigt wird. In meinem Experimente durchführen, haben die resultierenden Wasm-Module die richtige Größe für die Logik und die Teams von Emscripten und WebAssembly arbeiten daran, in Zukunft noch kleiner werden.

Sie erhalten Emscripten, indem Sie der Anleitung auf der Website oder über Homebrew. Wenn Sie ein Fan von einfach in Docker-Anwendungen wie ich zu schreiben und keine mit WebAssembly spielen zu können, gibt es eine Docker-Image, das Sie verwenden können stattdessen:

    $ docker pull trzeci/emscripten
    $ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>

Etwas Einfaches kompilieren

Nehmen wir das fast kanonische Beispiel für die Schreiben einer Funktion in C, die berechnet die n-te Fibonacci-Zahl:

    #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;
    }

Wenn Sie C kennen, sollte die Funktion selbst nicht allzu überraschend sein. Auch wenn Sie nicht mit C, aber nicht mit JavaScript vertraut sind, was hier passiert.

emscripten.h ist eine von Emscripten bereitgestellte Headerdatei. Wir brauchen sie nur, auf das EMSCRIPTEN_KEEPALIVE-Makro zugreifen können, bietet viel mehr Funktionen. Dieses Makro weist den Compiler an, eine Funktion nicht zu entfernen, selbst wenn sie angezeigt wird nicht verwendet werden. Wenn wir dieses Makro weglassen, wird die Funktion vom Compiler entfernt. – schließlich wird sie von niemandem genutzt.

Speichern wir das alles in einer Datei namens fib.c. Um sie in eine .wasm-Datei umzuwandeln, müssen Sie den Compiler-Befehl emcc von Emscripten verwenden:

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c

Schauen wir uns diesen Befehl an. emcc ist der Compiler von Emscripten. fib.c ist unser C -Datei. So weit, so gut. -s WASM=1 bittet Emscripten, uns eine Wasm-Datei zu senden anstelle einer asm.js-Datei verwenden. -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' weist den Compiler an, den cwrap()-Funktion in der JavaScript-Datei verfügbar – weitere Informationen zu dieser Funktion . -O3 weist den Compiler an, aggressiv zu optimieren. Sie können einen niedrigeren Wert um die Build-Dauer zu verringern. So erhalten Sie auch die da der Compiler nicht verwendeten Code nicht entfernt.

Nach dem Ausführen des Befehls sollten Sie eine JavaScript-Datei namens a.out.js und eine WebAssembly-Datei namens a.out.wasm. Die Wasm-Datei (oder "module") enthält unseren kompilierten C-Code und sollte relativ klein sein. Die Die JavaScript-Datei übernimmt das Laden und Initialisieren unseres Wasm-Moduls und eine schönere API bereitstellen. Bei Bedarf übernimmt er auch die Einrichtung des Stacks, Heaps und andere Funktionen, die normalerweise vom wenn Sie C-Code schreiben. Daher ist die JavaScript-Datei größer, mit einem Gewicht von 19 KB (~5 KB mit gzip).

Etwas Einfaches ausführen

Die einfachste Methode zum Laden und Ausführen Ihres Moduls ist die Verwendung des generierten JavaScript-Codes -Datei. Sobald Sie diese Datei geladen haben, Module weltweit die Ihnen zur Verfügung stehen. Verwenden Sie cwrap zum Erstellen einer nativen JavaScript-Funktion, die Parameter umwandelt, und die verpackte Funktion aufrufen. cwrap gewinnt Funktionsname, Rückgabetyp und Argumenttypen als Argumente in dieser Reihenfolge:

    <script src="a.out.js"></script>
    <script>
      Module.onRuntimeInitialized = _ => {
        const fib = Module.cwrap('fib', 'number', ['number']);
        console.log(fib(12));
      };
    </script>

Wenn Sie diesen Code ausführen, sollten Sie die Meldung „144“ sehen, in der Konsole angezeigt, das ist die zwölfte Fibonacci-Zahl.

Der heilige Gral: Compiling a C library

Bis jetzt wurde der von uns geschriebene C-Code mit Blick auf Wasm geschrieben. Ein Kern WebAssembly-Anwendungsfall ist jedoch, und ermöglichen es Entwicklern, diese im Web zu nutzen. In diesen Bibliotheken nutzen die Standardbibliothek von C, ein Betriebssystem, ein Dateisystem Dinge. Die meisten dieser Funktionen stehen Ihnen in Emscripten zur Verfügung, auch wenn es einige Einschränkungen.

Zurück zu meinem ursprünglichen Ziel: der Kompilierung eines Encoders für WebP in Wasm. Die für den WebP-Codec in C geschrieben ist und GitHub und in einigen umfassenden API-Dokumentation Das ist ein guter Ausgangspunkt.

    $ git clone https://github.com/webmproject/libwebp

Zunächst versuchen wir, WebPGetEncoderVersion() aus encode.h in JavaScript, indem Sie eine C-Datei namens webp.c schreiben:

    #include "emscripten.h"
    #include "src/webp/encode.h"

    EMSCRIPTEN_KEEPALIVE
    int version() {
      return WebPGetEncoderVersion();
    }

Mit diesem einfachen Programm können Sie testen, ob wir den Quellcode von libwebp abrufen können. da wir keine Parameter oder komplexen Datenstrukturen benötigen, rufen Sie diese Funktion auf.

Um dieses Programm zu kompilieren, müssen wir dem Compiler mitteilen, wo er libwebp-Headerdateien mit dem Flag -I und übergeben alle C-Dateien des libwebp installiert. Ich habe ganz ehrlich gesagt: Ich habe alle C Dateien, die ich finden konnte, und ließ mich auf den Compiler ließen, um alles zu entfernen, unnötig. Es schien hervorragend zu funktionieren!

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
        -I libwebp \
        webp.c \
        libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

Jetzt brauchen wir nur noch etwas HTML und JavaScript, um unser brandneues Modul zu laden:

<script src="/a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async (_) => {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
</script>

Die Versionsnummer der Korrektur wird in der output (Ausgabe):

Screenshot der Entwicklertools-Konsole mit der richtigen Version
Nummer.

Bild aus JavaScript in Wasm übertragen

Die Versionsnummer des Encoders zu bekommen, ist toll, aber die Codierung einer wäre wirklich beeindruckend, oder? Also los!

Die erste Frage, die wir uns beantworten müssen, lautet: Wie gelangen wir ins Wasmland? Wenn Sie sich die Codierungs-API von libwebp vor, wird erwartet, ein Bytearray im RGB-, RGBA-, BGR- oder BGRA-Format. Glücklicherweise hat die Canvas API getImageData(), das uns eine Uint8ClampedArray mit den Bilddaten im RGBA-Format:

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);
}

Jetzt geht es nur noch die Daten aus JavaScript in Wasm zu kopieren, Land. Dazu müssen zwei zusätzliche Funktionen verfügbar gemacht werden. Eine, die Daten zuordnet, Erinnerung für das Bild innerhalb des Wasm-Landes und eine, die es wieder freigibt:

    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 weist einen Zwischenspeicher für das RGBA-Bild zu, also 4 Byte pro Pixel. Der von malloc() zurückgegebene Zeiger ist die Adresse der ersten Speicherzelle von diesen Puffer. Wenn der Zeiger an das JavaScript-Land zurückgegeben wird, wird er nur eine Zahl. Nachdem wir die Funktion mit cwrap für JavaScript verfügbar gemacht haben, um den Anfang des Puffers zu finden und die Bilddaten zu kopieren.

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);

Großes Finale: Bild codieren

Das Bild ist jetzt in Wasm verfügbar. Es ist Zeit, den WebP-Encoder aufzurufen, machen seinen Job! Wenn Sie sich die WebP-Dokumentation, WebPEncodeRGBA scheint perfekt zu passen. Die Funktion führt einen Zeiger zum Eingabebild Dimensionen und eine Qualitätsoption zwischen 0 und 100 auswählen. Außerdem ordnet sie dem Ausgabepuffer, den wir mit WebPFree() freigeben müssen, sobald wir WebP-Bild erstellt.

Das Ergebnis des Codierungsvorgangs ist ein Ausgabepuffer und seine Länge. Weil Funktionen in C können keine Arrays als Rückgabetypen haben (es sei denn, wir dynamisch) auf ein statisches globales Array zurückgegriffen. Ich weiß, kein sauberes C Es basiert darauf, dass die Wasm-Zeiger 32 Bit breit sind, aber um die Dinge einfach. Ich denke, das ist eine gute Abkürzung.

    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];
    }

Jetzt können wir die Funktion „coding“ aufrufen, die Funktion die Zeiger- und Bildgröße an, fügen sie in einen JavaScript-Landzwischenspeicher ein alle Wasm-Land-Puffer, die wir dabei zugeteilt haben, freizugeben.

    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);

Je nach Größe Ihres Bildes kann ein Fehler auftreten, der Speicher kann nicht genug vergrößert werden, um sowohl das Eingabe- als auch das Ausgabebild unterzubringen:

Screenshot der Entwicklertools-Konsole mit einem Fehler.

Glücklicherweise finden Sie die Lösung für dieses Problem in der Fehlermeldung. Wir müssen nur Fügen Sie unserem Kompilierungsbefehl -s ALLOW_MEMORY_GROWTH=1 hinzu.

Sie haben das Lab erfolgreich abgeschlossen. Wir haben einen WebP-Encoder kompiliert und ein JPEG-Bild in WebP. Um zu beweisen, dass es funktioniert hat, können wir unseren Ergebnispuffer in ein Blob umwandeln und bei einem <img>-Element:

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);

Sieh mal, die Ehre eines neuen WebP-Bildes!

Netzwerkbereich in den Entwicklertools und das generierte Bild.

Fazit

Es ist kein Spaziergang im Park, um eine C-Bibliothek im Browser zum Laufen zu bringen, wenn Sie den gesamten Prozess und die Funktionsweise des Datenflusses verstehen, und die Ergebnisse können überwältigend sein.

WebAssembly eröffnet im Web viele neue Verarbeitungsmöglichkeiten. und Gaming. Denk daran, dass Wasm keine Patentlösung ist, auf alles angewendet werden kann, aber wenn Sie einen dieser Engpässe treffen, kann Wasm ein unglaublich hilfreiches Tool.

Bonusinhalte: Etwas Einfaches auf den großen Weg bringen

Wenn Sie die generierte JavaScript-Datei vermeiden möchten, können Sie an. Kehren wir zum Fibonacci-Beispiel zurück. Um sie selbst zu laden und auszuführen, Gehen Sie so vor:

<!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>

Von Emscripten erstellte WebAssembly-Module haben keinen Arbeitsspeicher zum Arbeiten es sei denn, Sie stellen Gedächtnis bereit. Die Art und Weise, wie Sie ein Wasm-Modul anything ist die Verwendung des imports-Objekts – dem zweiten Parameter des instantiateStreaming. Das Wasm-Modul hat Zugriff auf alle Inhalte im Inneren das imports-Objekt an, aber sonst nichts. Nach Konvention können Module der durch Emscripting kompilierten Umgebung:

  • Erstens: env.memory. Das Wasm-Modul erkennt das Äußere nicht der Welt sozusagen ein wenig Gedächtnis. Eingabetaste WebAssembly.Memory Er stellt einen (optional wachsenden) linearen Teil dar. Die Größe Parameter befinden sich in „in Einheiten von WebAssembly-Seiten“, d. h. der Code oben weist 1 Seite Arbeitsspeicher zu, wobei jede Seite eine Größe von 64 hat KiB Ohne Angabe von maximum Option ist der Speicher theoretisch unbegrenzt wachsen (Chrome hat derzeit feste Beschränkung von 2 GB). Bei den meisten WebAssembly-Modulen sollte kein maximal.
  • env.STACKTOP definiert, wo der Stack wachsen soll. Der Stack wird benötigt, um Funktionsaufrufe auszuführen und Arbeitsspeicher für lokale Variablen zuzuweisen. Da wir in unserem kleinen System Fibonacci-Programm haben, können wir einfach den gesamten Speicher als Stapel nutzen, STACKTOP = 0