Qu'est-ce que WebAssembly et d'où provient-il ?

Depuis que le Web est devenu une plate-forme non seulement dédiée aux documents, mais aussi aux applications, certaines des applications les plus avancées ont repoussé leurs limites avec les navigateurs Web. L'approche consistant à "se rapprocher du métal" en interagissant avec des langues de niveau inférieur afin d'améliorer les performances est présente dans de nombreux langages de niveau supérieur. Par exemple, Java dispose de l'interface native Java. Pour JavaScript, le langage de niveau inférieur est WebAssembly. Dans cet article, vous découvrirez ce qu'est le langage assembleur et son utilité sur le Web, puis comment WebAssembly a été créé avec la solution provisoire d'asm.js.

Langage d'assemblage

Avez-vous déjà programmé une programmation en langage assembleur ? En programmation informatique, le langage d'assemblage, souvent appelé simplement "Assembly" et couramment abrégé sous le terme "ASM" ou "asm", est tout langage de programmation de bas niveau présentant une correspondance très étroite entre les instructions du langage et les instructions du code machine de l'architecture.

Par exemple, si nous examinons les architectures Intel® 64 et IA-32 (PDF), l'instruction MUL (pour multiplication) effectue une multiplication non signée du premier opérande (opérande de destination) et du second (opérande source), puis stocke le résultat dans l'opérande de destination. Pour simplifier, l'opérande de destination est un opérande implicite situé dans le registre AX, tandis que l'opérande source se trouve dans un registre à usage général, tel que CX. Le résultat est à nouveau stocké dans le registre AX. Prenons l'exemple de code x86 suivant:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

À titre de comparaison, si vous avez pour objectif de multiplier les 5 et 10, vous écrirez probablement un code semblable à celui-ci en JavaScript:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

L'avantage du processus d'assemblage est qu'un code de bas niveau optimisé par une machine est beaucoup plus efficace que le code de haut niveau optimisé par l'humain. Dans le cas précédent, cela n'a pas d'importance, mais vous pouvez imaginer que la différence peut être importante pour des opérations plus complexes.

Comme son nom l'indique, le code x86 dépend de l'architecture x86. Et s'il existait un moyen d'écrire du code d'assemblage qui ne dépende pas d'une architecture spécifique, mais qui hériterait des avantages de l'assemblage en termes de performances ?

asm.js

La première étape pour écrire du code d'assemblage sans dépendance d'architecture a été asm.js, un sous-ensemble strict de JavaScript pouvant être utilisé comme langage cible de bas niveau et efficace pour les compilateurs. Ce sous-langage décrivait efficacement une machine virtuelle en bac à sable pour les langages non sécurisés comme C ou C++. Une combinaison de validation statique et dynamique a permis aux moteurs JavaScript d'utiliser une stratégie de compilation d'optimisation anticipée (AOT) pour un code asm.js valide. Le code écrit dans des langages de typage statique avec gestion manuelle de la mémoire (comme C) était traduit par un compilateur source-source tel que l'ancien Emscripten (basé sur LLVM).

Les performances ont été améliorées en limitant les fonctionnalités linguistiques à celles qui sont adaptées à l'AOT. Firefox 22 a été le premier navigateur à prendre en charge asm.js sous le nom d'OdinMonkey. Dans la version 61, Chrome est compatible avec asm.js. Bien que asm.js fonctionne toujours dans les navigateurs, il a été remplacé par WebAssembly. À ce stade, utiliser asm.js constituerait une alternative pour les navigateurs qui ne sont pas compatibles avec WebAssembly.

WebAssembly

WebAssembly est un langage de bas niveau de type assembly avec un format binaire compact qui s'exécute avec des performances quasi natives et fournit des langages tels que C/C++ et Rust, ainsi que de nombreux autres, avec une cible de compilation afin qu'ils s'exécutent sur le Web. La prise en charge des langages à gestion de mémoire tels que Java et Dart est en cours de développement et devrait bientôt être disponible, ou a déjà été introduite dans le cas de Kotlin/Wasm. WebAssembly est conçu pour s'exécuter avec JavaScript, ce qui permet aux deux de fonctionner ensemble.

Outre le navigateur, les programmes WebAssembly s'exécutent également dans d'autres environnements d'exécution grâce à WASI, l'interface système de WebAssembly, une interface système modulaire pour WebAssembly. WASI est conçu pour être portable entre différents systèmes d'exploitation, dans le but d'être sécurisé et de pouvoir s'exécuter dans un environnement de bac à sable.

Le code WebAssembly (code binaire, c'est-à-dire bytecode) est destiné à être exécuté sur une machine virtuelle (VM) portable. Le bytecode est conçu pour être plus rapide à analyser et exécuter que JavaScript, et pour offrir une représentation compacte du code.

L'exécution conceptuelle des instructions s'effectue à l'aide d'un compteur de programme classique qui progresse dans les instructions. En pratique, la plupart des moteurs Wasm compilent le bytecode Wasm en code machine, puis l'exécutent. Les instructions se divisent en deux catégories:

  • Des instructions de contrôle qui forment des constructions de contrôle et font sortir leurs valeurs d'argument de la pile, peuvent modifier le compteur de programme et transférer les valeurs de résultat dans la pile.
  • Des instructions simples qui font sortir les valeurs d'argument de la pile, appliquent un opérateur aux valeurs, puis transfèrent les valeurs de résultat dans la pile, puis avance implicitement du compteur de programme.

Pour en revenir à l'exemple précédent, le code WebAssembly suivant serait équivalent au code x86 présenté au début de l'article:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

Bien que asm.js soit implémenté dans un logiciel, c'est-à-dire que son code peut s'exécuter dans n'importe quel moteur JavaScript (même s'il n'est pas optimisé), WebAssembly nécessitait une nouvelle fonctionnalité sur laquelle tous les fournisseurs de navigateurs se sont mis d'accord. Annulé en 2015 et sorti pour la première fois en mars 2017, WebAssembly est devenu une recommandation du W3C le 5 décembre 2019. Le W3C maintient la norme grâce aux contributions de tous les principaux fournisseurs de navigateurs et d'autres parties intéressées. Depuis 2017, tous les navigateurs sont compatibles.

WebAssembly a deux représentations: textuelle et binaire. Ce que vous voyez ci-dessus est la représentation textuelle.

Représentation textuelle

La représentation textuelle est basée sur des expressions S et utilise généralement l'extension de fichier .wat (pour le format de texte WebAssembly). Si vous le vouliez vraiment, vous pouvez l'écrire à la main. En prenant l'exemple de multiplication présenté ci-dessus et en le rendant plus utile en ne codant plus en dur les facteurs, vous pouvez probablement donner un sens au code suivant:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

Représentation binaire

Le format binaire qui utilise l'extension de fichier .wasm n'est pas destiné à être vu par les humains, et encore moins par la création humaine. À l'aide d'un outil tel que wat2wasm, vous pouvez convertir le code ci-dessus dans la représentation binaire suivante. (Les commentaires ne font généralement pas partie de la représentation binaire, mais ils sont ajoutés par l'outil wat2wasm pour une meilleure compréhension.)

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

Compiler sur WebAssembly

Comme vous pouvez le constater, ni .wat, ni .wasm ne sont particulièrement adaptés aux humains. C'est là qu'un compilateur comme Emscripten entre en jeu. Il vous permet de compiler à partir de langages de niveau supérieur tels que C et C++. Il existe d'autres compilateurs pour d'autres langages comme Rust et bien d'autres. Prenons l'exemple du code C suivant:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

En règle générale, vous compilez ce programme C avec le compilateur gcc.

$ gcc hello.c -o hello

Une fois Emscripten installé, compilez-le dans WebAssembly à l'aide de la commande emcc et de presque les mêmes arguments:

$ emcc hello.c -o hello.html

Un fichier hello.wasm et le fichier wrapper HTML hello.html sont alors créés. Lorsque vous diffusez le fichier hello.html à partir d'un serveur Web, "Hello World" s'affiche dans la console DevTools.

Il existe également un moyen de compiler dans WebAssembly sans le wrapper HTML:

$ emcc hello.c -o hello.js

Comme précédemment, le fichier hello.wasm sera créé, mais cette fois, un fichier hello.js au lieu du wrapper HTML. Pour effectuer le test, exécutez le fichier JavaScript hello.js obtenu, par exemple avec Node.js:

$ node hello.js
Hello World

Plus d'infos

Cette brève introduction de WebAssembly n'est que la partie visible de l'iceberg. Pour en savoir plus sur WebAssembly, consultez la documentation de WebAssembly sur MDN et la documentation d'Emscripten. En vérité, l'utilisation de WebAssembly ressemble un peu à l'article Comment dessiner un mème hibou, d'autant plus que les développeurs Web qui connaissent bien HTML, CSS et JavaScript ne maîtrisent pas nécessairement les langages à compiler, comme le C. Heureusement, il existe des canaux comme le tag webassembly de StackOverflow où des experts se feront un plaisir de vous aider si vous posez la question gentiment.

Remerciements

Cet article a été lu par Jakob Kummerow, Derek Schuff et Rachel Andrew.