Aller au contenu principal

Compilation & langages formels · L3 · Section 5/8

WASM

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#WebAssembly

WebAssembly (WASM) est un format binaire portable, exécutable dans le navigateur et dans des runtimes autonomes (wasmtime, WasmEdge). Il offre une exécution proche du natif dans un bac à sable strict: pas d'accès disque, pas d'accès réseau, pas de sortie écran, tout passe par des fonctions importées explicitement.

Prérequis: page Génération de code (l'IR à pile et le module WAT assemblé à la main).

Objectifs d'apprentissage

  • Lire un module WAT: sections, paramètres, variables locales, pile.
  • Décrire le cycle de vie compile → validate → instantiate → call.
  • Connaître les types i32/i64/f32/f64 et le modèle d'exécution à pile.

#Format binaire, représentation texte

WASM existe en deux formes: le binaire (.wasm, ce que le navigateur télécharge) et le texte (.wat, lisible, converti par wat2wasm). Le binaire commence par la magie \0asm puis la version, puis une suite de sections: types, fonctions, mémoires, exports, code. La validation précède toujours l'exécution: un module mal formé (pile déséquilibrée, type incorrect, saut hors fonction) est rejeté avant toute exécution.

#Le fil conducteur, version WASM

Le programme let x = 5; let y = x*2; y+3 compilé à la page précédente devient:

watwat

1(module2  (func (export "main") (result i32)3    (local i32 i32)4    i32.const 55    local.set 06    local.get 07    i32.const 28    i32.mul9    local.set 110    local.get 111    i32.const 312    i32.add))

Appeler main renvoie 13, comme l'interpréteur Python de la page AST et comme la simulation de la page codegen. Trois exécutions, un même résultat: c'est la définition opérationnelle d'un compilateur correct.

#Exemple: Fibonacci récursif

Un module un peu plus riche, avec paramètre, branchement et appel récursif:

watwat

1(module2  (func $fib (export "fib") (param $n i32) (result i32)3    (if (result i32) (i32.lt_s (local.get $n) (i32.const 2))4      (then (local.get $n))5      (else6        (i32.add7          (call $fib (i32.sub (local.get $n) (i32.const 1)))8          (call $fib (i32.sub (local.get $n) (i32.const 2)))))))9  (func (export "main") (result i32)10    (call $fib (i32.const 10))))

Lecture guidée:

  • (param $n i32) déclare le paramètre; (result i32) promet exactement un i32 sur la pile en sortie. Cette promesse est vérifiée par le validateur, pas par la bonne volonté du programmeur.
  • En WAT « plié » (s-expression), l'ordre d'exécution reste postfixe: dans (i32.add (call …) (call …)), les deux appels s'exécutent avant l'addition, exactement comme l'IR à pile de la page précédente.
  • (if (result i32) c (then a) (else b)) est la version structurée du if (result i32) … else … end de l'exercice codegen: chaque branche pousse une valeur.

fib(10) vaut 55. Attention: la complexité est exponentielle (environ phi^n appels, phi ≈ 1,618), ce qui en fait aussi un bon test de charge pour un moteur JIT.

#Cycle de vie côté navigateur

javascriptjavascript

1// 1. compiler+valider le flux, 2. instancier, 3. appeler.2const { instance } = await WebAssembly.instantiateStreaming(3  fetch('fib.wasm'), {}                    // imports: aucun ici4)5console.log(instance.exports.fib(10))      // 55

instantiateStreaming compile pendant le téléchargement (le serveur doit servir Content-Type: application/wasm). Le second argument liste les imports: fonctions, mémoire ou tables que l'hôte fournit au module. C'est l'unique frontière du bac à sable: un module WASM ne peut appeler que ce qu'on lui a explicitement passé.

#Playground interactif

Le composant ci-dessous exécute un vrai module WASM dans votre navigateur: le module binaire est décodé, instancié, et sa fonction exportée appelée.

Le module binaire ci-dessous est l'exact équivalent de l'exemple fib ci-dessus, assemblé par wat2wasm depuis le même WAT et exporté sous le nom f (c'est le nom que le composant appelle). L'appel renvoie 55, la valeur de fib(10).

Exécute un module WASM (base64) qui exporte f().

Le binaire est court (71 octets) parce que le format est compact: les noms de sections et les types sont encodés par des entiers, et seuls les exports gardent un nom lisible. Pour vérifier l'équivalence, désassemblez-le avec wasm2wat sur le même binaire: vous retrouverez le (if (result i32) …) de l'exemple.

#Mémoire et types

  • Types: i32, i64, f32, f64. Pas de i8/i16 calculatoires: les octets vivent en mémoire, chargés puis étendus. Les entiers sont non signés par défaut dans certaines opérations, signés dans d'autres (i32.div_s vs i32.div_u): le suffixe compte.
  • Mémoire linéaire: un tableau d'octets adressable de 0 à la taille courante, croissante par pages de 64 KiO (memory.grow). C'est le seul état mutable global d'un module; C/C++/Rust y posent leur tas.
  • Appels: la pile d'appels est gérée par le moteur, pas accessible au module. La récursion profonde lève un piège call stack exhausted plutôt qu'un débordement mémoire.

#Limites

  • Interopérabilité: les appels JS ↔ WASM ont un coût de conversion; on garde la frontière grossière (peu d'appels, beaucoup de calcul par appel).
  • Débogage: possible (source maps, WebAssembly.toString(), DevTools) mais moins mature que JS; nommer ses fonctions WAT aide.
  • Taille: un module peut dépasser quelques Mo; compression et découpage (dynamic import) limitent l'impact.

La page suivante détaille les contraintes d'exécution côté navigateur (mémoire, threads, CSP).

#Exercice : square en WAT, puis en binaire

#Instructions

  1. Écrivez un module WAT exportant square(x) qui renvoie x * x avec i32.mul.
  2. Vérifiez mentalement l'invariant de pile: un paramètre en entrée, un résultat en sortie.
  3. Testez-le dans le playground ci-dessous avec plusieurs valeurs, y compris négatives.

#Correction

watwat

1(module2  (func (export "square") (param $x i32) (result i32)3    local.get $x4    local.get $x5    i32.mul))

Trace de pile pour square(7): local.get $x pousse 7, le second local.get $x pousse 7, i32.mul consomme les deux et pousse 49. La pile finale vaut [49], conforme au (result i32) promis. Pour square(-3), le résultat est 9: i32.mul sur deux négatifs donne un positif en complément à deux.

Le module ci-dessous est ce même code, assemblé par wat2wasm, avec deux exports: square (le paramètre) et f (la fonction sans argument que le composant appelle, et qui renvoie square(-3), soit 9).

Exécute un module WASM (base64) qui exporte f().

#Mini‑quiz

Que fait le validateur avant l'exécution d'un module ?
Que fait le validateur avant l'exécution d'un module ?