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 à leurs limites. L'approche consistant à se rapprocher du matériel en interagissant avec des langages de bas niveau pour améliorer les performances est courante dans de nombreux langages de haut niveau. Par exemple, Java dispose de l'interface Java Native Interface. Pour JavaScript, ce langage de bas niveau 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éé à l'aide de la solution provisoire asm.js.
Langage d'assemblage
Avez-vous déjà programmé en langage assembleur ? En programmation informatique, le langage d'assemblage, souvent appelé simplement "assembleur" et couramment abrégé en ASM ou asm, est n'importe quel langage de programmation de bas niveau avec une très forte correspondance entre les instructions du langage et les instructions du code machine de l'architecture.
Par exemple, si l'on examine 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), et stocke le résultat dans l'opérande de destination. Pour faire simple, l'opérande de destination est un opérande implicite situé dans le registre AX
, et 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 à celui-ci en JavaScript :
const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;
L'avantage de passer par l'assemblage est que ce code de bas niveau et optimisé pour les machines est beaucoup plus efficace que le code de 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. Et s'il existait un moyen d'écrire du code assembleur qui ne dépend pas d'une architecture spécifique, mais qui hérite des avantages de l'assembleur en termes de performances ?
asm.js
La première étape pour écrire du code assembleur sans dépendances d'architecture a été asm.js, un sous-ensemble strict de JavaScript qui pouvait être utilisé comme langage cible efficace de bas niveau pour les compilateurs. Ce sous-langage décrivait efficacement une machine virtuelle sandboxée pour les langages non sécurisés en mémoire comme C ou C++. Une combinaison de validation statique et dynamique permettait aux moteurs JavaScript d'utiliser une stratégie de compilation d'optimisation ahead-of-time (AOT) pour le code asm.js valide. Le code écrit dans des langages à typage statique avec gestion manuelle de la mémoire (comme C) était traduit par un compilateur source-à-source tel que early Emscripten (basé sur LLVM).
Les performances ont été améliorées en limitant les fonctionnalités linguistiques à celles compatibles avec la compilation AOT. Firefox 22 a été le premier navigateur à prendre en charge asm.js, 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, asm.js ne serait utilisé que comme alternative pour les navigateurs non compatibles avec WebAssembly.
WebAssembly
WebAssembly est un langage de bas niveau de type assembleur avec un format binaire compact qui s'exécute avec des performances quasi natives. Il fournit aux langages tels que C/C++ et Rust, ainsi qu'à de nombreux autres, une cible de compilation pour qu'ils s'exécutent sur le Web. La compatibilité avec les langages à gestion de 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 aux côtés de JavaScript, ce qui permet aux deux 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, qui est une interface système modulaire pour WebAssembly. WASI est conçu pour être portable sur différents systèmes d'exploitation, avec l'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 destiné à être exécuté sur une machine virtuelle (VM) à pile virtuelle 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 à l'aide d'un compteur de programme traditionnel 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 :
- Les instructions de contrôle qui forment des constructions de contrôle et retirent leurs valeurs d'argument de la pile peuvent modifier le compteur de programme et insérer des valeurs de résultat dans la pile.
- Instructions simples qui dépilent la ou les valeurs de leur argument, appliquent un opérateur aux valeurs, puis empilent la ou les valeurs du résultat, suivies d'une progression 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 qu'asm.js est entièrement implémenté dans un 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 se sont mis 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 gère la norme avec les contributions de tous les principaux fournisseurs de navigateurs et d'autres parties intéressées. Depuis 2017, la compatibilité avec les navigateurs est universelle.
WebAssembly possède deux représentations : textuelle et binaire. La représentation textuelle est affichée ci-dessus.
Représentation textuelle
La représentation textuelle est basée sur les expressions S et utilise généralement l'extension de fichier .wat
(pour le format texte WebAssembly text). 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 lu par des humains, et encore moins à être créé par eux. À 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 en WebAssembly
Comme vous le voyez, ni .wat
ni .wasm
ne sont particulièrement faciles à lire pour un humain. 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 de 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é, compilez-le en WebAssembly à l'aide de la commande emcc
et de presque les mêmes arguments :
$ emcc hello.c -o hello.html
Cela crée un fichier hello.wasm
et le fichier wrapper HTML hello.html
. Lorsque vous diffusez le fichier hello.html
à partir d'un serveur Web, "Hello World"
s'affiche dans la console d'outils de développement.
Il existe également un moyen de compiler vers WebAssembly sans le wrapper HTML :
$ emcc hello.c -o hello.js
Comme précédemment, cela créera un fichier hello.wasm
, mais cette fois-ci 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 introduction à WebAssembly n'est que la partie émergée de l'iceberg.
Pour en savoir plus sur WebAssembly, consultez la documentation WebAssembly sur MDN et la documentation Emscripten. Pour être honnête, travailler avec WebAssembly peut donner l'impression de suivre le meme "Comment dessiner un hibou", surtout que les développeurs Web qui connaissent HTML, CSS et JavaScript ne sont pas forcément versés dans les langages à compiler, comme C. Heureusement, il existe des canaux comme le tag webassembly
de StackOverflow, où les experts se feront un plaisir de vous aider si vous leur demandez gentiment.
Remerciements
Cet article a été relu par Jakob Kummerow, Derek Schuff et Rachel Andrew.