l'encodage binaire et une représentation textuelle associée
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.