Émission d'une bibliothèque C vers Wasm

Il peut arriver que vous souhaitiez utiliser une bibliothèque disponible uniquement sous forme de code C ou C++. En général, c'est là que l'on abandonne. Eh bien, plus maintenant, car nous avons maintenant Emscripten et WebAssembly (ou Wasm) !

Chaîne d'outils

Je me suis fixé comme objectif de trouver un moyen de compiler du code C existant pour Wasm. Comme il y a eu du bruit autour du backend Wasm de LLVM, J'ai commencé à m’y plonger. Alors que vous pouvez obtenir des programmes simples à compiler. de cette façon, la seconde, vous voulez utiliser la bibliothèque standard de C ou même compiler plusieurs fichiers, vous rencontrerez probablement des problèmes. Cela m'a conduit à la spécialité ce que j'ai appris:

Même si Emscripten était auparavant un compilateur C-to-asm.js, il est depuis passé à cible Wasm et est au processus de transition vers le backend LLVM officiel en interne. Emscripten fournit également une Implémentation compatible avec Wasm de la bibliothèque standard C Utilisez Emscripten. Il comporte beaucoup de tâches cachées, émule un système de fichiers, assure la gestion de la mémoire, encapsule OpenGL avec WebGL, beaucoup de choses que vous n'avez vraiment pas besoin de développer par vous-même.

Vous avez peut-être l'impression de ne pas être surchargé, mais j'ai bien peur le compilateur Emscripten supprime tout ce qui n'est pas nécessaire. Dans mon les modules Wasm sont dimensionnés en fonction de la logique et les équipes d'Emscripten et de WebAssembly ils seront encore plus petits à l’avenir.

Pour obtenir Emscripten, suivez les instructions site Web ou via Homebrew. Si vous êtes fan de des commandes dockerisées comme moi et que je ne veux pas installer des choses sur votre système juste d'utiliser WebAssembly, il existe un système Image Docker que vous pouvez utiliser à la place:

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

Compilation d'un contenu simple

Prenons l'exemple presque canonique de l'écriture d'une fonction en C qui calcule le nième nombre de 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;
    }

Si vous connaissez C, la fonction elle-même ne devrait pas être trop surprenante. Même si vous vous ne connaissez pas le C, mais vous connaissez JavaScript. ce qui se passe ici.

emscripten.h est un fichier d'en-tête fourni par Emscripten. Nous n’en avons besoin que pour ont accès à la macro EMSCRIPTEN_KEEPALIVE, mais qu'elle offre beaucoup plus de fonctionnalités. Cette macro indique au compilateur de ne pas supprimer une fonction, même si elle apparaît non utilisé. Si nous omettez cette macro, le compilateur optimisera la fonction car personne ne l'utilise après tout.

Enregistrons tout cela dans un fichier nommé fib.c. Pour le transformer en fichier .wasm, nous vous devez utiliser la commande de compilation d'Emscripten emcc:

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

Examinons cette commande. emcc est le compilateur d'Emscripten. fib.c est notre C . Jusque-là, tout va bien. -s WASM=1 indique à Emscripten de nous fournir un fichier Wasm au lieu d'un fichier asm.js. -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' indique au compilateur de laisser Fonction cwrap() disponible dans le fichier JavaScript (en savoir plus sur cette fonction) plus tard. -O3 indique au compilateur d'effectuer une optimisation agressive. Vous pouvez choisir des valeurs inférieures pour réduire la durée de compilation, mais cela rend aussi les bundles plus grand, car le compilateur risque de ne pas supprimer le code inutilisé.

Après avoir exécuté la commande, vous devriez obtenir un fichier JavaScript appelé a.out.js et un fichier WebAssembly appelé a.out.wasm. Le fichier Wasm (ou « module ») contient notre code C compilé et devrait être assez petit. La JavaScript se charge du chargement et de l'initialisation du module Wasm, en fournissant une API plus conviviale. Si nécessaire, il se charge également de configurer de la pile, le tas de mémoire et d'autres fonctionnalités habituellement attendues par la lors de l'écriture du code C. Ainsi, le fichier JavaScript est un peu plus grand et pesant 19 Ko (environ 5 Ko compressés avec gzip).

Exécuter quelque chose de simple

Le moyen le plus simple de charger et d'exécuter votre module consiste à utiliser le code JavaScript généré . Une fois ce fichier chargé, Module monde entier à votre disposition. Utilisez cwrap pour créer une fonction native JavaScript qui se charge des paramètres de conversion vers un élément compatible avec C et qui appelle la fonction encapsulée. cwrap remporte la nom de la fonction, type renvoyé et types d'argument en tant qu'arguments, dans cet ordre:

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

Si vous exécuter ce code, vous devriez voir le « 144 » de la console, qui est le 12e nombre de Fibonacci.

Le Saint Graal: compiler une bibliothèque C

Jusqu'à présent, le code C que nous avons écrit était conçu pour Wasm. Un noyau mais dans ce cas d'utilisation, WebAssembly consiste à prendre l'écosystème existant bibliothèques et permettent aux développeurs de les utiliser sur le Web. Souvent, ces bibliothèques s’appuient sur la bibliothèque standard de C, un système d’exploitation, un système de fichiers et d’autres les choses. Emscripten propose la plupart de ces fonctionnalités, bien que certaines Limites.

Revenons à mon objectif initial: compiler un encodeur pour WebP vers Wasm. La source du codec WebP est écrit en C et disponible sur GitHub, ainsi que d'excellentes Documentation de l'API C'est un bon point de départ.

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

Pour commencer, essayons d'exposer WebPGetEncoderVersion() à partir de encode.h en JavaScript en écrivant un fichier C appelé webp.c:

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

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

Il s'agit d'un bon programme simple pour tester si nous pouvons obtenir le code source de libwebp à compiler, car nous n'avons pas besoin de paramètres ni de structures de données complexes appelle cette fonction.

Pour compiler ce programme, nous devons indiquer au compilateur où il peut trouver les fichiers d'en-tête de libwebp à l'aide de l'indicateur -I et lui transmettre tous les fichiers C de libwebp dont il a besoin. Pour être honnête, je viens de donner tout le do les fichiers que je pouvais trouver et je comptais sur le compilateur pour supprimer tout ce qui était inutile. Cela semble fonctionner parfaitement !

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

À présent, nous n'avons besoin que d'un peu de code HTML et JavaScript pour charger notre tout nouveau module:

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

Le numéro de version de correction apparaît output:

Capture d&#39;écran de la console DevTools montrant la bonne version
numéro.

Extraire une image de JavaScript dans Wasm

Obtenir le numéro de version de l'encodeur est une bonne chose, mais encoder un serait plus impressionnante, n'est-ce pas ? Allons-y.

La première question à laquelle nous devons répondre est la suivante: comment transférer l'image dans le pays de Wasm ? En consultant API d'encodage de libwebp, il attend un tableau d'octets au format RVB, RVBA, BGR ou BGRA. Heureusement, l'API Canvas getImageData(), qui nous donne un Uint8ClampedArray contenant les données d'image au format RVBA:

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

Maintenant, c'est « uniquement » il s'agit de copier les données de JavaScript dans Wasm sur le terrain. Pour cela, nous devons présenter deux fonctions supplémentaires. Celui qui alloue pour l'image à l'intérieur de Wasm Land et une autre qui la libère:

    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 alloue un tampon pour l'image RVBA, soit 4 octets par pixel. Le pointeur renvoyé par malloc() est l'adresse de la première cellule de mémoire de ce tampon. Lorsque le pointeur est renvoyé vers la page JavaScript, il est traité comme qu'un nombre. Après avoir exposé la fonction à JavaScript à l'aide de cwrap, nous pouvons utiliser ce numéro pour trouver le début de notre tampon et copier les données de l'image.

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

Version finale: encoder l'image

L'image est désormais disponible au pays de Wasm. Il est temps d'appeler l'encodeur WebP faire son travail ! En consultant Documentation WebP, WebPEncodeRGBA semble correspondre parfaitement. Elle utilise un pointeur vers l'image d'entrée ses dimensions, et une option de qualité comprise entre 0 et 100. Il alloue également un tampon de sortie que nous devrons libérer à l'aide de WebPFree() une fois que nous aurons avec l'image WebP.

Le résultat de l'opération d'encodage est un tampon de sortie et sa longueur. En effet, les fonctions en C ne peuvent pas avoir de tableaux comme types renvoyés (sauf si nous allouons de la mémoire de manière dynamique), j'ai eu recours à un tableau global statique. Je sais, pas propre C (en fait, il s'appuie sur le fait que les pointeurs Wasm sont larges de 32 bits), mais pour garder les choses simple, je pense que c'est un raccourci juste.

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

Maintenant que tout est en place, nous pouvons appeler la fonction d'encodage, récupérer le pointeur de la souris et la taille de l'image, le placer dans notre propre tampon JavaScript-Land, tous les tampons de terre Wasm que nous avons alloués dans le processus.

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

Selon la taille de votre image, vous pouvez rencontrer une erreur lorsque Wasm n'augmente pas suffisamment la mémoire pour accueillir à la fois l'image d'entrée et l'image de sortie:

Capture d&#39;écran de la console DevTools montrant une erreur.

Heureusement, la solution à ce problème se trouve dans le message d'erreur. Nous devons juste nous ajoutons -s ALLOW_MEMORY_GROWTH=1 à notre commande de compilation.

Et voilà ! Nous avons compilé un encodeur WebP et transcodé une image JPEG WebP. Pour prouver que cela a fonctionné, nous pouvons transformer notre tampon de résultat en blob et utiliser sur un élément <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);

Voici la gloire d'une nouvelle image WebP !

le panneau réseau des outils de développement et l&#39;image générée.

Conclusion

Ce n'est pas une promenade dans le parc de faire fonctionner une bibliothèque C dans le navigateur, mais une fois vous comprenez le processus global et le fonctionnement du flux de données, il devient plus facile et les résultats peuvent être époustouflants.

WebAssembly ouvre de nombreuses nouvelles possibilités de traitement sur le Web. et jouer à des jeux vidéo. Gardez à l'esprit que Wasm n'est pas une solution miracle qui devrait mais si vous rencontrez l'un de ces goulots d'étranglement, Wasm un outil incroyablement utile.

Contenu bonus: courir le plus difficilement

Si vous souhaitez éviter le fichier JavaScript généré, vous pouvez auxquelles vous souhaitez vous connecter. Revenons à l'exemple de Fibonacci. Pour le charger et l'exécuter nous-mêmes, nous pouvons effectuer les opérations suivantes:

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

Les modules WebAssembly créés par Emscripten n'ont pas de mémoire pour fonctionner. sauf si vous leur fournissez de la mémoire. La façon dont vous fournissez un module Wasm n'importe quoi consiste à utiliser l'objet imports (le deuxième paramètre de la méthode instantiateStreaming. Le module Wasm peut accéder à tout ce qu'il contient l'objet imports, mais rien d'autre en dehors de celui-ci. Par convention, les modules compilées en emscriptant s'attendent à ce que le code JavaScript environnement:

  • Tout d'abord, il y a env.memory. Le module Wasm ne tient pas compte de l'extérieur du monde pour ainsi dire, il a donc besoin de mémoire pour travailler. Entrée WebAssembly.Memory Il représente un élément de mémoire linéaire (qui peut être développé de manière facultative). Taille se trouvent dans des "unités de pages WebAssembly", c'est-à-dire que le code ci-dessus alloue 1 page de mémoire, chaque page ayant une taille de 64 KiB : Sans maximum en théorie, le taux de croissance de la mémoire est illimité (Chrome dispose actuellement une limite stricte de 2 Go). La plupart des modules WebAssembly ne devraient pas avoir besoin de définir un maximum.
  • env.STACKTOP définit où la pile est censée commencer à se développer. Pile est nécessaire pour effectuer des appels de fonction et pour allouer de la mémoire aux variables locales. Comme nous ne faisons aucune manigance de gestion de la mémoire dynamique dans notre petit programme Fibonacci, nous pouvons simplement utiliser l'intégralité de la mémoire comme pile. STACKTOP = 0