WebAssembly est un outil permettant d’exécuter d’autres langages que JavaScript sur des pages web. Auparavant, lorsqu’on souhaitait exécuter du code dans le navigateur afin d’interagir avec les différents composants d’une page web, JavaScript était la seule solution.

C’est pourquoi, lorsqu’on dit que WebAssembly est rapide, on compare sa rapidité à celle de JavaScript. Cela ne signifie pas pour autant qu’il faut utiliser l’un ou l’autre et pas les deux.

En fait, on s’attend à ce que les développeurs utilisent aussi bien WebAssembly et JavaScript au sein de la même application. Même si vous n’écrivez pas du WebAssembly, vous pouvez en tirer parti.

Les modules WebAssembly définissent des fonctions qui peuvent être utilisées depuis JavaScript. Si aujourd’hui, vous téléchargez un module npm comme lodash et que vous utilisez les fonctions qu’il fournit via son API, demain, vous serez aussi capable de télécharger et d’exploiter des modules WebAssembly.

Voyons maintenant comment créer des modules WebAssembly et comment les utiliser depuis JavaScript.

Quelle place pour WebAssembly ?

Dans l’article précédent sur l’assembleur, nous avons vu comment les compilateurs traitaient les langages de programmation de haut niveau pour les traduire en code machine.

L'utilisation de la représentation intermédiaire avec les différents composants

Quel est le rôle de WebAssembly dans cet environnement ?

On peut penser qu’il s’agit simplement d’un autre langage assembleur vers lequel compiler. D’une certaine façon, c’est vrai mais chacun de ces autres langages (x86, ARM) correspond à une architecture machine particulière.

Lorsqu’on envoie du code à exécuter sur une machine à travers le Web, on ne connaît pas l’architecture cible sur laquelle le code sera exécuté.

WebAssembly est donc légèrement différent des autres langages assembleurs. Il s’agit d’un langage machine pour une machine théorique et non pour une machine physique.

Pour cette raison, les instructions WebAssembly sont parfois appelées instructions virtuelles. Elles sont beaucoup plus proches du code machine que n’importe quel code source JavaScript avec un langage qui ressemble à l’intersection de ce qui est effectué efficacement sur les architectures matérielles répandues. Mais ces instructions ne correspondent pas non plus à un langage machine spécifique d’une architecture matérielle donnée.

La place de WebAssembly par rapport à cette représentation intermédiaire

C’est le navigateur qui télécharge le code WebAssembly. Ensuite, il effectue la transition (plus courte) entre WebAssembly et le code assembleur de la machine sur laquelle il est exécuté.

Compiler vers .wasm

L’ensemble d’outils de compilation qui prend le mieux en charge WebAssembly actuellement s’appelle LLVM. Il existe différents environnements frontaux (front-ends) ou de fin de chaîne (back-ends) qui peuvent être utilisés avec LLVM.

Note : la plupart des développeurs de modules WebAssembly utiliseront des langages tels que C et Rust avant de compiler en WebAssembly. Toutefois, il existe d’autres méthodes qui permettent de créer des modules WebAssembly. Il existe par exemple un outil expérimental qui permet de compiler un module WebAssembly en utilisant TypeScript. On peut aussi écrire directement du WebAssembly en utilisant sa représentation textuelle.

Prenons le scénario où on développe un module en C pour le compiler en WebAssembly. On pourrait utiliser le module frontal clang pour passer de la représentation en C à la représentation intermédiaire LLVM. Une fois qu’on a obtenu la RI LLVM, LLVM peut la comprendre et effectuer certaines optimisations.

Pour passer de la RI (représentation intermédiaire) LLVM à celle de WebAssembly, il nous faut un composant de fin de chaîne. Il existe un composant en cours de développement pour le projet LLVM. Ce composant devrait être finalisé sous peu mais reste délicat à utiliser aujourd’hui.

Il existe un autre outil, intitulé Emscripten, qui est actuellement plus facile à utiliser. Cet outil possède son propre composant de fin de chaîne qui peut produire du code WebAssembly en compilant vers une cible intermédiaire (appelée asm.js) puis en convertissant ce résultat en WebAssembly. Sous le capot d’Emscripten, on retrouve en fait LLVM et on peut donc passer d’un composant de fin de chaîne à l’autre à partir d’Emscripten.

L'ensemble d'outils de compilation

Emscripten inclut de nombreux outils et bibliothèques supplémentaires pour le portage de bases de code en C/C++. Il s’agit donc plus d’un kit de développement logiciel (NDT ou SDK pour Software Developer Kit, plus fréquemment utilisé) que d’un simple compilateur. Les développeurs système ont par exemple l’habitude d’utiliser un système de fichiers depuis lequel on peut lire des fichiers et sur lequel on peut en écrire. Pour ce faire, Emscripten peut simuler un système de fichier en utilisant IndexedDB.

Quels que soient les outils que vous utilisez, le résultat final sera un fichier dont l’extension sera .wasm. Nous verrons par la suite la structure d’un fichier .wasm mais pour commencer, voyons comment on peut l’utiliser en JavaScript.

Charger un module WebAssembly en JavaScript

Le fichier .wasm contient le module WebAssembly et peut être chargé en JavaScript. Au moment de l’écriture de ces lignes, le processus de chargement est un peu compliqué :

function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =>
    response.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
    results.instance
  );
}

Pour plus de détails, vous pouvez consulter la documentation associée.

Nous travaillons à simplifier cette étape en améliorant les outils et en intégrant les modules WebAssembly dans des gestionnaires de modules comme webpack ou dans des outils de chargement comme SystemJS. Nous pensons que le chargement des modules WebAssembly peut être aussi simple que celui que nous connaissons aujourd’hui pour les modules JavaScript.

Il existe toutefois une différence fondamentale entre les modules WebAssembly et les modules JavaScript. Actuellement, les fonctions WebAssembly permettent uniquement d’utiliser des nombres (entiers ou flottants) comme paramètres et comme valeurs de retour.

On ne passe que des entiers

Pour manipuler des types de donnée plus complexes (des chaînes de caractères par exemple), il faut utiliser la mémoire du module WebAssembly.

Si vous travaillez principalement avec JavaScript, l’accès direct à la mémoire n’est pas forcément un concept très familier. Des langages de plus bas niveau tels que C, C++ ou Rust permettent de gérer la mémoire manuellement. La mémoire d’un module WebAssembly permet de simuler le tas (NDT ou « heap » en anglais, également usité) qu’on trouverait dans ces langages.

Pour cela, on utilise un type d’objet JavaScript : les ArrayBuffer. Un tableau tampon (NDT « array buffer » en anglais) est un tableau d’octets. Les indices des positions dans ce tableau servent d’adresses mémoire.

Si on veut passer une chaîne de caractères depuis le code JavaScript vers le code WebAssembly, on convertit les caractères en utilisant les codes d’encodage correspondants. Ensuite, on écrit ces codes dans le tableau représentant la mémoire. Les indices du tableau étant des entiers, on peut les passer à la fonction WebAssembly. Cela permet ainsi d’utiliser l’indice du première caractère de la chaîne comme un pointeur.

Pour compenser, on sert des entiers comme pointeurs

Il est probable que lorsque quelqu’un développera un module WebAssembly destiné à des développeurs web, il ajoutera une enveloppe (wrapper) avec des fonctions utilitaires pour ce module afin que le développeur web n’ait pas à se soucier de la gestion de la mémoire.

Si vous souhaitez en savoir plus, n’hésitez pas à consulter notre documentation à propos de la gestion de la mémoire en WebAssembly.

La structure d’un fichier .wasm

Si vous écrivez du code avec un langage de programmation de haut niveau pour le compiler en WebAssembly, vous n’avez pas besoin de savoir quelle est la structure d’un module WebAssembly. Ceci étant dit, comprendre les notions de base s’avère souvent utile.

Si ce n’est pas déjà fait, nous vous conseillons de lire l’article précédent sur l’assembleur (le troisième de cette série).

Voici une fonction, écrite en C, que nous allons transformer en WebAssembly :

int add42(int num) {
  return num + 42;
}

Vous pouvez essayer d’utiliser WASM Explorer afin de compiler cette fonction.

Si vous ouvrez le fichier .wasm obtenu (et que votre éditeur le permet), vous verrez alors quelque chose comme :

00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B

Ce qu’on voit ici est la représentation « binaire » du module (avec des guillemets de précaution car généralement, le contenu est affiché en notation hexadécimale, mais on peut facilement la convertir en notation binaire ou dans un format plus lisible pour un humain).

Voici par exemple à quoi ressemble num + 42 :

La décomposition de l'opération selon les différents niveaux de représentation

Le fonctionnement du code : un processeur à pile

Au cas où vous vous demanderiez, voici ce que feraient ces instructions :

La décomposition de l'opération selon les différents niveaux de représentation

  • On prend la valeur du premier paramètre et on la met sur la pile.
  • On met une valeur constante sur la pile
  • On prend les deux valeurs sur le haut de la pile, on les additionne et on met le résultat sur la pile.

On peut voir ici que l’opération add n’indique pas l’origine des valeurs qu’elle manipule. En effet, WebAssembly est ce qu’on appelle un automate à pile. Cela signifie que les valeurs nécessaires à une opération sont empilées avant que l’opération soit appliquée.

Pour l’addition, WebAssembly sait combien de valeurs sont nécessaires. L’addition a besoin de deux valeurs et on prend donc les deux valeurs situées sur le haut de la pile. Cela signifie que l’instruction pour l’addition peut être courte (un seul octet) car il n’est pas nécessaire d’indiquer les registres de source ou de destination. Cela permet de réduire la taille du fichier .wasm et ainsi de réduire le temps nécessaire à son téléchargement.

Bien que WebAssembly soit conçu comme un automate à pile, ce n’est pas comme ça qu’il fonctionne réellement sur la machine physique. Lorsque le navigateur traduit le code WebAssembly en code machine pour l’architecture sur laquelle il est exécuté, le code utilisera les registres. Étant donné que le code WebAssembly ne détaille pas les registres, cela fournit une plus grande flexibilité au navigateur qui peut choisir la meilleure stratégie d’allocation des registres pour la machine utilisée.

Les sections du module

En plus de la fonction add42, on trouve d’autres parties dans le fichier .wasm. Ces parties sont appelées des « sections ». Certaines de ces sections sont nécessaires quel que soit le module et d’autres sont optionnelles.

Voici la liste des sections obligatoires :

  1. Type : cette section contient la signature des fonctions qui sont définies dans ce module ou importées.
  2. Function : cette section contient un index de chaque fonction qui est définie dans ce module.
  3. Code : cette section contient le corps de chaque fonction définie dans ce module.

Voici la liste des sections optionnelles :

  1. Export : cette section permet de rendre accessibles la mémoire, les tables et les variables globales pour d’autres modules WebAssembly et pour JavaScript. Cela permet d’avoir des modules compilés séparément et de les lier dynamiquement. C’est en quelque sorte la version WebAssembly d’une .dll
  2. Import : cette section définit les fonctions, mémoires, tables et variables globales qui doivent être importées depuis d’autres modules WebAssembly ou depuis du JavaScript.
  3. Start : une fonction qui sera automatiquement exécutée au chargement du module WebAssembly (l’équivalent d’une fonction main)
  4. Global : cette section définit les variables globales du module.
  5. Memory : cette section définit la mémoire utilisée par ce module.
  6. Table : cette section permet de faire un pont avec des fonctions situées en dehors du module WebAssembly telles que des fonctions JavaScript. Cela est notamment utile pour permettre des appels de fonction indirects.
  7. Data : cette section initialise la mémoire locale ou importée.
  8. Element : cette section initialise une table locale ou importée.

Pour plus de détails quant au fonctionnement des sections, vous trouverez plus d’explications dans la documentation.

La suite

Maintenant qu’on sait comment fonctionnent les modules WebAssembly, voyons pourquoi WebAssembly est rapide.

À propos de Lin Clark

Lin est ingénieure au sein de l’équipe Mozilla Developer Relations. Elle bidouille avec JavaScript, WebAssembly, Rust et Servo et crée des bandes dessinées sur le code.