WebAssembly, un format exécutable pour le Web

Avec les mises à jour de navigateurs arrivées ces dernières semaines, il existe désormais un standard pour l’exécution de code haute performance via le Web. WebAssembly est en effet maintenant disponible sur Edge, Safari, Chrome et Firefox.

C'est la promesse d'un environnement d’exécution standard sur toutes les machines, quel que soit le matériel et l'OS.

Certains y voient la future plateforme pour le déploiement d'applications universelles — pour mobiles, mais pas seulement. Une plateforme qui permettrait une performance très proche de celle des applications natives. D'autres s'inquiètent de la fragmentation à prévoir des efforts et des méthodes de développement, et d'une érosion supplémentaire des valeurs fondamentales du Web.

WebAssembly est le résultat d'un important travail de coordination entre les principaux éditeurs de navigateurs Web. C'est un format binaire destiné à permettre ce qui se faisait jusqu'à maintenant uniquement via JavaScript, du moins dans les navigateurs les plus répandus : la fourniture de code via le Web pour exécution dans le navigateur.

WebAssembly est conçu pour être plus performant que JavaScript :

  • le format est plus compact que JavaScript même compressé (donc prend moins de temps à télécharger puis à analyser)
  • les données sont fortement typées, ce qui facilite les optimisations automatiques de code
  • la compilation et l'optimisation une fois sur la machine cible sont plus rapides car le code pré-compilé avant mise en ligne est plus proche du langage machine, et a subi une première optimisation

GWT, CoffeeScript, TypeScript, Parenscript et consorts, "langages" compilés vers JavaScript, avaient déjà fait de JavaScript une sorte de langage machine du Web. Mozilla avait développé asm.js, un sous-ensemble plus performant de JavaScript, dédié à servir de cible à des compilateurs comme LLVM. Les outils de minification mutilaient de leur côté le code JavaScript pour le rendre le plus compact possible, de façon à gagner en temps de transfert puis en temps d'analyse. Google, avec son projet NaCL, permettait l’exécution de code natif dans un environnement contrôlé à l'intérieur de Chrome.

WebAssembly est en quelque sorte la fusion concertée, l'aboutissement logique de tout cela. Sa performance d’exécution devrait permettre des applications nouvelles, autour de tout ce qui nécessite une puissance de calcul importante côté client, comme le traitement audio ou vidéo, la 3D ou le chiffrement.

Pour l'instant, ce que les navigateurs ont tous implémenté est ce qui avait été défini comme le "MVP" de WebAssembly. Ce MVP entérine :

Les outils permettent actuellement de compiler du C, du C++ ou du Rust. En théorie, tout langage peut désormais être compilé pour le navigateur, d'autant plus facilement s'il est compilable via LLVM, qui sait générer du WebAssembly. À condition toutefois qu'il s'agisse d'un langage qui ne nécessite pas de Garbage Collection, puisque cet aspect, s'il est prévu, n'est pas disponible dans le MVP. Parmi les autres fonctionnalités en cours de développement, on notera la gestion des exceptions, la gestion de fils d'exécution et la manipulation directe du DOM (qui n'est actuellement possible qu'en (re)passant par JavaScript).

Pour jouer un peu avec WebAssembly, le plus simple est d'utiliser WasmFiddle, qui permet, en restant dans son navigateur, d'écrire du code C d'un côté, du code JS de l'autre, puis de compiler le code C en WebAssembly pour pouvoir exécuter le tout.

Le "Hello World" avec WebAssembly est un peu déroutant. En effet, WebAssembly ne permet pour le moment d’interagir directement ni avec le DOM ni avec la console du navigateur. Pour écrire quelque chose, il faut donc passer par l'interface avec JavaScript. Autre hic, WebAssembly n'a que 4 types : flottants et entiers, 32 et 64 bits. Il n'y a donc pas de type chaîne de caractère ou même tableau, ce qui fait que le texte "Hello World" va devoir passer de WebAssembly à JavaScript sous la forme d'un pointeur qui indiquera où dans la mémoire WebAssembly lire des entiers correspondant aux codes des caractères. Un char * dans notre programme C, devient une suite d'i32 en WebAssembly. On passe à JavaScript un pointeur vers cette suite en mémoire qu'il lira pour en faire une chaîne de caractères. Tout simple !

Commençons donc par écrire notre fonction "Hello World" en C :

char* hello() {
  return "Hello World !";
}

WasmFiddle la compile et nous donne le code WASM suivant, dans lequel on voit la définition de notre fonction, son "exportation" du module et l'exportation faite automatiquement par WasmFiddle de la mémoire dans une propriété "memory". Les exportations sont ce qui rend ces éléments accessibles depuis JavaScript.

(module
  (table 0 anyfunc)
  (memory $0 1)
  (data (i32.const 16) "Hello World !\00")
  (export "memory" (memory $0))
  (export "hello" (func $hello))
  (func $hello (result i32)
    (i32.const 16)
  )
)

Il nous faut alors écrire la partie Javascript, qui s'appuie sur le fait que WasmFiddle met notre code WebAssembly compilé dans un array Javascript "wasmCode" :

function makeStringFromASCIICodes(memory, pointer) {
  let s = "";
  for (i = pointer; memory[i]!==0; i++) {
    s += String.fromCharCode(memory[i]);
  }
  return s;
}

let wasmModule = new WebAssembly.Instance(new WebAssembly.Module(wasmCode));
let memory = new Uint8Array(wasmModule.exports.memory.buffer);
let pointer = wasmModule.exports.hello();
alert(makeStringFromASCIICodes(memory, pointer));

J'ai mis l'ensemble tout fait dans le WasmFiddle suivant : https://wasdk.github.io/WasmFiddle/?1aax07

Et si on veut compiler la même chose sur sa machine ? La boite à outils WebAssembly de dcodeIO permet de démarrer plus rapidement qu'avec seulement le compilateur emscripten, que les tutoriels officiels proposent d'utiliser.

J'ai créé un fichier hello.c contenant le même code que précédemment, mais avec un include supplémentaire, et un export devant la définition de ma fonction. Les deux sont des conventions de l'outil WebAssembly de dcodeIO :

#include <webassembly.h>

export char* hello() {
  return "Hello World !";
}

Pour compiler tout ça :

wa-compile -o hello.wasm hello.c

Puis je peux regarder la tête de mon programme avec :

wa-disassemble hello.wasm
(module
  (type $0 (func (result i32)))
  (import "env" "memory" (memory $0 1))
  (table 0 anyfunc)
  (data (i32.const 4) "0\'")
  (data (i32.const 16) "Hello World !")
  (export "hello" (func $0))
  (func $0 (type $0) (result i32)
    (i32.const 16)
 )
)

Pour faire fonctionner l'ensemble dans un navigateur, j'utilise le même code JavaScript que précédemment, mais je dois reproduire ce que faisait WasmFiddle : inclure le code compilé dans un array JavaScript, et ajouter l'initialisation de la mémoire. J'aurais pu éviter cela en utilisant la librairie JS fournie dans la boite à outils de dcodeIO, qui initialise la mémoire pour nous et inclut quelques utilitaires, mais le faire à la main permet de comprendre ce qui est nécessaire.

Cela donne le HTML suivant :


<html>
  <script type="text/javascript">
    var wasmCode = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80, 0x80, 0x00, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x02, 0x8f, 0x80, 0x80, 0x80, 0x00, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x01, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x04, 0x84, 0x80, 0x80, 0x80, 0x00, 0x01, 0x70, 0x00, 0x00, 0x07, 0x89, 0x80, 0x80, 0x80, 0x00, 0x01, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x09, 0x81, 0x80, 0x80, 0x80, 0x00, 0x00, 0x0a, 0x8a, 0x80, 0x80, 0x80, 0x00, 0x01, 0x84, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x10, 0x0b, 0x0b, 0xa6, 0x80, 0x80, 0x80, 0x00, 0x03, 0x00, 0x41, 0x04, 0x0b, 0x04, 0x30, 0x27, 0x00, 0x00, 0x00, 0x41, 0x0c, 0x0b, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x10, 0x0b, 0x0e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x20, 0x21, 0x00]);

    function makeStringFromASCIICodes(memory, pointer) {
      let s = "";
      for (i = pointer; memory[i]!==0; i++) {
        s += String.fromCharCode(memory[i]);
      }
      return s;
    }

    let wasmMemory = new WebAssembly.Memory({ initial: 1 });
    let wasmModule = new WebAssembly.Instance(new WebAssembly.Module(wasmCode), {env: {memory: wasmMemory}});
    let memory = new Uint8Array(wasmMemory.buffer);
    let pointer = wasmModule.exports.hello();
    alert(makeStringFromASCIICodes(memory, pointer));
  </script>
</html>

Pour obtenir le code WebAssembly sous forme d'un array JavaScript, j'ai combiné xxd et sed :

xxd -c 10000 -p hello.wasm | sed -e 's/\w\w/0x\0, /g'

Et voilà le travail ! C'est un peu plus compliqué que je ne l'aurais imaginé, mais on y arrive.

WebAssembly a beaucoup de potentiel. Mais la promesse d'un environnement d’exécution universel pour les clients Web était à peu près la raison d'être principale de Java à ses débuts. J'espère que WebAssembly n'annonce pas le retour des applets, devenues enfin "cool", dans une de ces répétitions de l'histoire qu'on voit souvent en informatique.

Étant l'aboutissement logique de la minification et de l'utilisation de JavaScript ou asm.js comme cible de compilation, WebAssembly participe malheureusement à la perte d'ouverture sur le Web : avec le code compilé, on est de plus en plus loin du Web du partage libre d'information, du Web où l'on pouvait savoir comment n'importe quel site était fait en en regardant le code source directement dans son navigateur, du Web où l'éternelle application CRUD n'était pas un "client lourd" dans mon navigateur.

WebAssembly risque d’entraîner l'utilisation du Web pour des choses dont on pourrait bien se demander l’intérêt qu'il y apporte par rapport au code natif, si ce n'est un contrôle de la distribution du code. À l'heure où les DRMs sont également universellement supportés par les navigateurs, je crains que WebAssembly ne soit l'une des briques qui permette d'enfermer de plus en plus les utilisateurs dans un environnement sur lequel ils n'ont aucun contrôle. L'une des briques qui permette d’accélérer encore la "cloudification" des logiciels et des données, c'est à dire du service comme substitut au logiciel, dépossédant les usagers de la possibilité de savoir ce qui est fait de leurs données, de vérifier le code qui s’exécute, de le modifier, ainsi que d'avoir la certitude que les fonctionnalités d'aujourd'hui seront toujours disponibles demain.

À l'inverse, si WebAssembly peut permettre de remplacer des applications par des sites Web "augmentés", comme les développements autour d'HTML5 le permettaient déjà de plus en plus, j'en serais très heureux.

J'espère que nous saurons trouver collectivement le bon équilibre, mais je m'attends à une rude bataille.