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

Depuis que le Web est devenu une plate-forme pour les documents et les applications, certaines des applications les plus avancées ont poussé les navigateurs Web à leurs limites. L'approche consistant à interagir avec des langues de niveau inférieur afin d'améliorer les performances est utilisée dans de nombreux langages de niveau supérieur. 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 découvrirez ensuite comment WebAssembly a été créé via la solution provisoire d'asm.js.

Langage d'assemblage

Avez-vous déjà programmé en langage d'assemblage ? En programmation informatique, le langage d'assemblage, souvent appelé simplement "assembleur" et communément abrégé en ASM ou asm, est tout langage de programmation de bas niveau présentant une correspondance très forte entre les instructions du langage et les instructions du 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. Très simplement, 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 5 et 10, vous écririez 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 tel code de bas niveau optimisé pour les machines est beaucoup plus efficace que du code de haut niveau optimisé par l'humain. Dans le cas précédent, ce n'est pas grave, mais vous pouvez imaginer que pour des opérations plus complexes, la différence peut être importante.

Comme son nom l'indique, le code x86 dépend de l'architecture x86. Et s'il existait un moyen d'écrire du code assembleur qui ne dépendait 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 assembly sans dépendances 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 a décrit efficacement une machine virtuelle en bac à sable pour les langages non sécurisés tels que C ou C++. Une combinaison de validations statiques et dynamiques a permis aux moteurs JavaScript d'utiliser une stratégie de compilation anticipée (ou "AOT") optimisée pour le code asm.js valide. Le code écrit dans des langages de type statique avec gestion manuelle de la mémoire (comme le C) a été traduit par un compilateur source-source tel que le premier Emscripten (basé sur LLVM).

Les performances ont été améliorées en limitant les fonctionnalités linguistiques à ceux qui sont compatibles avec AOT. Firefox 22 est le premier navigateur compatible avec asm.js. Il est sorti sous le nom d'OdinMonkey. Chrome est compatible avec asm.js dans la version 61. Même si asm.js fonctionne toujours dans les navigateurs, il a été remplacé par WebAssembly. À ce stade, vous devriez utiliser asm.js pour remplacer les navigateurs qui ne sont pas compatibles avec WebAssembly.

WebAssembly

WebAssembly est un langage de type assembly de bas niveau avec un format binaire compact qui s'exécute avec des performances presque natives et 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 à mémoire tels que Java et Dart est en cours de développement. Elle devrait être disponible prochainement, ou elle est déjà disponible, comme dans le cas de Kotlin/Wasm. WebAssembly est conçu pour s'exécuter parallèlement à 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 WebAssembly, une interface système modulaire pour WebAssembly. WASI est conçu pour être portable sur 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 avoir une représentation de code compacte.

L'exécution conceptuelle des instructions se fait par le biais d'un compteur de programme traditionnel 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:

  • Instructions de contrôle qui créent des commandes de formulaire, font sortir leurs valeurs d'argument de la pile, modifient le compteur du programme et transfèrent les valeurs des résultats dans la pile.
  • Des instructions simples qui extraient leurs valeurs d'argument de la pile, appliquent un opérateur aux valeurs, puis transmettent les valeurs du résultat à la pile, et enfin font progresser implicitement le compteur du programme.

Pour en 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.

Bien qu'asm.js soit implémenté entièrement dans le 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 publié pour la première fois en mars 2017, WebAssembly est devenu une recommandation 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é des navigateurs est universelle.

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 Text Web). Si vous le souhaitez vraiment, vous pouvez l'écrire à la main. En prenant l'exemple de multiplication ci-dessus et en le rendant plus utile en ne codant plus les facteurs en dur, 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 utilisant l'extension de fichier .wasm n'est pas destiné à être vu par les humains, et encore moins à leur création. À l'aide d'un outil comme 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 conviviaux. 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 sur WebAssembly à l'aide de la commande emcc et d'arguments presque identiques:

$ emcc hello.c -o hello.html

Cette opération entraîne la création d'un fichier hello.wasm et du fichier de wrapper HTML hello.html. 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 sur WebAssembly sans le wrapper HTML:

$ emcc hello.c -o hello.js

Comme précédemment, un fichier hello.wasm sera créé, mais cette fois, 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 visible de l'iceberg. Pour en savoir plus sur WebAssembly, consultez la documentation WebAssembly sur MDN et la documentation Emscripten. À vrai dire, l'utilisation de WebAssembly peut ressembler à la page How to Drawings an owl meme, d'autant plus que les développeurs Web familiarisés avec HTML, CSS et JavaScript ne maîtrisent pas nécessairement les langages à compiler à partir de langages tels que C. Heureusement, il existe des chaînes comme la balise webassembly de StackOverflow, où des experts sont souvent ravis de vous aider.

Remerciements

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