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

Depuis que le Web est devenu une plate-forme non seulement pour les documents, mais aussi pour les applications, certaines des applications les plus avancées ont poussé les navigateurs Web dans leurs derniers retranchements. L'approche consistant à "s'approcher du métal" en établissant une interface avec des langages de bas niveau afin d'améliorer les performances est rencontrée dans de nombreux langages de haut niveau. Par exemple, Java dispose de l'interface native Java. Pour JavaScript, ce langage de niveau inférieur est WebAssembly. Dans cet article, vous découvrirez ce qu'est le langage d'assemblage et pourquoi il peut être utile sur le Web. Vous apprendrez ensuite comment WebAssembly a été créé via la solution provisoire d'asm.js.

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

Par exemple, en examinant les architectures Intel® 64 et IA-32 (PDF), l'instruction MUL (pour la multiplication) effectue une multiplication non signée du premier opérande (opérande de destination) et du deuxième opérande (opérande source), puis stocke le résultat dans l'opérande de destination. De façon très simplifiée, 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 deviez multiplier 5 et 10, vous écririez probablement un code semblable au suivant en JavaScript :

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

L'avantage de l'assemblage est que ce code bas niveau et optimisé pour les machines est beaucoup plus efficace que le code haut niveau et optimisé pour les humains. Dans le cas précédent, cela n'a pas d'importance, mais vous pouvez imaginer que pour des opérations plus complexes, la différence peut être significative.

Comme son nom l'indique, le code x86 dépend de l'architecture x86. Que se passerait-il si vous pouviez écrire du code assembleur qui ne dépendait pas d'une architecture spécifique, mais qui hériterait des avantages de performances de l'assemblage ?

asm.js

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

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

WebAssembly

WebAssembly est un langage d'assemblage de bas niveau avec un format binaire compact qui s'exécute avec des performances quasi natives. Il fournit des langages tels que C/C++ et Rust, et bien d'autres avec une cible de compilation pour qu'ils s'exécutent sur le Web. La prise en charge des langages gérés par la mémoire tels que Java et Dart est en cours de développement et devrait être disponible prochainement, ou l'est déjà, comme dans le cas de Kotlin/Wasm. WebAssembly est conçu pour s'exécuter avec JavaScript, ce qui permet aux deux technologies de fonctionner ensemble.

En plus du navigateur, les programmes WebAssembly s'exécutent également dans d'autres environnements d'exécution grâce à WASI, l'interface système WebAssembly, une interface système modulaire pour WebAssembly. WASI est conçu pour être portable entre les systèmes d'exploitation, avec pour objectif 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 conçu pour être exécuté sur une machine virtuelle (VM) portable. Le bytecode est conçu pour être analysé et exécuté plus rapidement que le code JavaScript, et pour présenter une représentation de code compacte.

L'exécution conceptuelle des instructions s'effectue via un compteur de programme classique qui avance 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:

  • Les instructions de contrôle qui forment des constructions de contrôle et extraient la ou les valeurs d'argument de la pile peuvent modifier le compteur de programme et placer la ou les valeurs de résultat sur la pile.
  • Instructions simples qui extraient la ou les valeurs d'argument de la pile, appliquent un opérateur aux valeurs, puis placent la ou les valeurs de résultat sur la pile, suivies d'un avancement implicite du compteur de programme.

Pour revenir à l'exemple précédent, le code WebAssembly suivant serait équivalent au code x86 du 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.

Alors que asm.js est entièrement implémenté en logiciel, c'est-à-dire que son code peut s'exécuter dans n'importe quel moteur JavaScript (même non optimisé), WebAssembly nécessitait de nouvelles fonctionnalités sur lesquelles tous les fournisseurs de navigateurs étaient d'accord. Annoncé en 2015 et publié 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 avec les contributions de tous les principaux fournisseurs de navigateurs et d'autres parties concernées. Depuis 2017, la compatibilité avec les navigateurs est universelle.

WebAssembly possède 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 texte de l'Assemblage Web). Si vous le souhaitez, vous pouvez l'écrire à la main. En reprenant l'exemple de multiplication ci-dessus et en le rendant plus utile en ne codant plus en dur les facteurs, vous pouvez probablement comprendre le 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 utilisé par l'utilisateur, et encore moins à être créé par lui. À l'aide d'un outil tel que wat2wasm, vous pouvez convertir le code ci-dessus en représentation binaire suivante. (Les commentaires ne font généralement pas partie de la représentation binaire, mais 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 vers WebAssembly

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

#include <stdio.h>

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

En général, vous compilez ce programme C avec le compilateur gcc.

$ gcc hello.c -o hello

Une fois Emscripten installé, vous le compilez en WebAssembly à l'aide de la commande emcc et des mêmes arguments presque identiques :

$ 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 en WebAssembly sans le wrapper HTML :

$ emcc hello.c -o hello.js

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

$ node hello.js
Hello World

En savoir plus

Cette brève présentation de WebAssembly n'est que la partie immergée de l'iceberg. Pour en savoir plus sur WebAssembly, consultez la documentation WebAssembly sur MDN et la documentation Emscripten. À vrai dire, travailler avec WebAssembly peut ressembler un peu au mème "How to draw an owl" (Comment dessiner une chouette), en particulier parce que les développeurs Web qui connaissent HTML, CSS et JavaScript ne sont pas nécessairement familiers avec les langages à compiler à partir de C. Heureusement, il existe des canaux comme la balise webassembly de StackOverflow, où les experts se feront souvent un plaisir de vous aider si vous leur demandez gentiment.

Remerciements

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