TP03 : Introduction à WebAssembly
Avec le TP précédent, vous avez implémenté un runtime pour le WebAssembly et vous
avez utilisé un code binaire .wasm simple qui additionne deux nombres.
Dans ce TP, vous allez écrire le code source de l’addition en WebAssembly et vous le compilerez
pour obtenir le fichier binaire .wasm.
Structure d’un fichier source WebAssembly
Les fichiers sources WebAssembly ont l’extension .wat pour WebAssembly Text. Le format
de ces fichiers est spécifié dans le chapitre Text Format
de la spécification WebAssembly et se base sur le format des S-expression.
Voici un exemple de fichier .wat qui implémente l’addition de deux nombres :
;; SPDX-FileCopyrightText: 2025 Jacques Supcik <jacques.supcik@hefr.ch>
;;
;; SPDX-License-Identifier: Apache-2.0 OR MIT
(module
(func $add (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
(export "add" (func $add))
)
À faire
- Compilez le code ci-dessus dans un fichier
.wasmavec la commandewat2wasmdu toolkitwabt. - Validez le fichier
.wasmavec la commandewasm-validate - Observez le résultat avec la commande
wasm-objdump -sxd - Observez le résultat avec la commande
wasm-decompile - Avec votre navigateur web, allez sur le WebAssembly Code Explorer et chargez votre fichier
.wasmpour en observer la structure.
Dans le TP précédent, nous avons mis à dispositions plusieurs fonctions pour permettre des interactions entre le runtime et le code WebAssembly. Il s’agit des éléments suivants:
- une fonction
OpenInput - une fonction
ReadInt(addr) - une fonction
eot() - une fonction
WriteChar(ch) - une fonction
WriteInt(val, len) - une fonction
WriteLn() - un bloc mémoire de 64kB
memory - une variable globale pour le
stack_pointer
Pour les utiliser dans le code WebAssembly, nous devons les importer dans le fichier .wat :
;; SPDX-FileCopyrightText: 2025 Jacques Supcik <jacques.supcik@hefr.ch>
;;
;; SPDX-License-Identifier: Apache-2.0 OR MIT
(module
(import "sys" "OpenInput" (func $open_input))
(import "sys" "ReadInt" (func $read_int (param i32)))
(import "sys" "eot" (func $eot (result i32)))
(import "sys" "WriteChar" (func $write_char (param i32)))
(import "sys" "WriteInt" (func $write_int (param i32 i32)))
(import "sys" "WriteLn" (func $write_ln))
(import "env" "memory" (memory 1))
(import "env" "__stack_pointer" (global $sp (mut i32)))
;; ...
)
Note
Le nom des modules (par exemple sys ou env) ainsi que celui les fonctions (comme OpenInput ou ReadInt) n’est pas important.
Ce qui compte c’est que l’ordre des éléments importés soit le même que celui exporté par le runtime.
Dites 42 ! (a.k.a “Hello Word”)
Familiarisez-vous avec les bases de WebAssembly: https://young.github.io/intro-to-web-assembly/, en particulier le chapitre sur la stack
Écrivez un programme WebAssembly qui affiche le nombre 42 en utilisant le runtime du TP précédent.
À faire
- Créez un fichier
say42.wat - Ajoutez le code ci-dessus pour importer les fonctions du runtime
- Étudiez les instructions du WebAssembly.
- Étudiez en particulier
i32.constetcall - Ajoutez une fonction
say42qui affiche le nombre 42 - Assurez vous que la fonction
say42soit exportée et qu’elle ne prenne aucun paramètre - Compilez le fichier
.waten un fichier.wasm - Refaite les opérations de validation et de décompilation
- Testez avec le runtime du TP précédent.
solution
;; SPDX-FileCopyrightText: 2025 Jacques Supcik <jacques.supcik@hefr.ch>
;;
;; SPDX-License-Identifier: Apache-2.0 OR MIT
(module
(import "sys" "OpenInput" (func $open_input))
(import "sys" "ReadInt" (func $read_int (param i32)))
(import "sys" "eot" (func $eot (result i32)))
(import "sys" "WriteChar" (func $write_char (param i32)))
(import "sys" "WriteInt" (func $write_int (param i32 i32)))
(import "sys" "WriteLn" (func $write_ln))
(import "env" "memory" (memory 1))
(import "env" "__stack_pointer" (global $sp (mut i32)))
(func (export "say42")
i32.const 42
i32.const 5
call $write_int
call $write_ln
)
)
Les variables
Contrairement à la plupart des microprocesseurs, Le WebAssembly ne possède pas de registres, et travaille avec une stack. Nous utilisons cette stack pour passer les opérandes aux opérations et pour passer les arguments aux fonctions. L’utilisation d’une stack est assez répandu en informatique. Le Zuse Z4, un de premier ordinateurs, conçu par Konrad Zuse en 1945, utilisait dàjà une stack pour les opérations.

Les calculatrices HP utilisent ce modèle connus sous le nom de de Notation polonaise inverse depuis les années 70.

Pour les variables locales, nous utilisons aussi une stack mais ce n’est pas la même que celle utilisée pour les opérations.
Le runtime nous donne accès à un bloc mémoire de 64KB. Nous allons utiliser ce bloc mémoire pour les variables globales et les variables locales de notre programme. La figure ci-dessous illustre l’organisation de la mémoire.
Les variables globales sont placées au début de la mémoire et les variables locales sont placées dans la stack à
la fin du bloc mémoire. La variable globale __stack_pointer pointe sur la dernière case occupée de la stack.
On a donc un modèle de stack de type full, descending similaire à celui spécifié dans l’ABI des processeurs ARM.
L’addition de deux nombres
On souhaite maintenant écrire le programme d’addition utilisé dans le TP précédent. En pseudocode, ça donne quelque chose comme ça:
Faire de la place sur la stack pour 3 variables (x, y et z)
Appeler OpenInput
Appeler ReadInt avec l'adresse de x
Appeler ReadInt avec l'adresse de y
Calculer x + y et sauver le résultat dans z
Appeler WriteInt avec la valeur de z
Appeler WriteLn
Rendre l'espace de la stack
Pour faire de la place sur la stack pour une variable, nous soustrayons 4 (la taille en byte d’un entier) du pointeur de pile. En WebAssembly, ça donne le code suivant:
global.get $sp
i32.const 4
i32.sub
global.set $sp
Pour notre exemple, nous avons besoin de 3 variables :
xà l’adresse$spyà l’adresse$sp + 4zà l’adresse$sp + 8
Si on veut appeler ReadInt avec l’adresse de y, on peut écrire:
;; put the address of y (sp+4) on the stack and call ReadInt
global.get $sp
i32.const 4
i32.add
call $read_int
Pour calculer z = z + 1, on peut écrire:
;; put the address of z (sp+8) on the stack (we will use it later)
global.get $sp
i32.const 8
i32.add
;; get the value of z
global.get $sp
i32.const 8
i32.add
i32.load
;; add 1
i32.const 1
i32.add
;; save the result back to z (we use the address put on the stack before)
i32.store
A faire
- Ecrivez le code
add.watpour additionner deux nombres fournis par le runtime - Compilez le fichier
.waten un fichier.wasm - Testez avec le runtime du TP précédent.
solution
;; SPDX-FileCopyrightText: 2025 Jacques Supcik <jacques.supcik@hefr.ch>
;;
;; SPDX-License-Identifier: Apache-2.0 OR MIT
(module
(import "sys" "OpenInput" (func $open_input))
(import "sys" "ReadInt" (func $read_int (param i32)))
(import "sys" "eot" (func $eot (result i32)))
(import "sys" "WriteChar" (func $write_char (param i32)))
(import "sys" "WriteInt" (func $write_int (param i32 i32)))
(import "sys" "WriteLn" (func $write_ln))
(import "env" "memory" (memory 1))
(import "env" "__stack_pointer" (global $sp (mut i32)))
(func (export "add")
;; allocate space for 3 integers (x, y, z). 3 * 4 bytes = 12 bytes
global.get $sp
i32.const 12
i32.sub
global.set $sp
;; call OpenInput
call $open_input
;; put the address of z (sp + 8) on the stack for the assignment z := x + y
global.get $sp
i32.const 8
i32.add
;; put the address of x (sp) on the stack and call ReadInt
global.get $sp
i32.const 0
i32.add
call $read_int
;; put the address of y (sp + 4) on the stack and call ReadInt
global.get $sp
i32.const 4
i32.add
call $read_int
;; load x from memory
global.get $sp
i32.const 0
i32.add
i32.load
;; load y from memory
global.get $sp
i32.const 4
i32.add
i32.load
;; add x and y
i32.add
;; store the result in z
i32.store
;; load z from memory
global.get $sp
i32.const 8
i32.add
i32.load
;; put 5 on the stack and call WriteInt
i32.const 5
call $write_int
;; call WriteLn
call $write_ln
;; free stack space
global.get $sp
i32.const 12
i32.add
global.set $sp
)
)
Les boucles
Écrivez maintenant un programme en WebAssembly qui affiche les nombres entiers positifs impairs strictement inférieurs au nombre passé en entrée. Par exemple :
uv run oberon0-rt -- print_odd_numbers.wasm run 11
devrait afficher:
1
3
5
7
9
Si le nombre passé en entrée est négatif ou inférieur à 2, le programme ne doit rien afficher.
À faire
- Écrivez le code
print_odd_numbers.watpour afficher les nombres impairs - Compilez le fichier
.waten un fichier.wasm - Testez avec le runtime du TP précédent.
solution
;; SPDX-FileCopyrightText: 2025 Jacques Supcik <jacques.supcik@hefr.ch>
;;
;; SPDX-License-Identifier: Apache-2.0 OR MIT
(module
(import "sys" "OpenInput" (func $open_input))
(import "sys" "ReadInt" (func $read_int (param i32)))
(import "sys" "eot" (func $eot (result i32)))
(import "sys" "WriteChar" (func $write_char (param i32)))
(import "sys" "WriteInt" (func $write_int (param i32 i32)))
(import "sys" "WriteLn" (func $write_ln))
(import "env" "memory" (memory 1))
(import "env" "__stack_pointer" (global $sp (mut i32)))
(func (export "run")
;; allocate space for 2 integers
;; SP -> i
;; SP + 4 -> max
global.get $sp
i32.const 8
i32.sub
global.set $sp
;; call OpenInput
call $open_input
;; ReadInt(max)
global.get $sp
i32.const 4
i32.add
call $read_int
;; i = 1
global.get $sp
i32.const 0
i32.add
i32.const 1
i32.store
(block $b1
;; check if i >= max (initial check)
global.get $sp
i32.const 0
i32.add
i32.load
global.get $sp
i32.const 4
i32.add
i32.load
i32.ge_s
br_if $b1
;; While loop
(loop $l1
;; print i
global.get $sp
i32.const 0
i32.add
i32.load
i32.const 5
call $write_int
call $write_ln
;; i = i + 2
global.get $sp
i32.const 0
i32.add
global.get $sp
i32.const 0
i32.add
i32.load
i32.const 2
i32.add
i32.store
;; repeat while i < max
global.get $sp
i32.const 0
i32.add
i32.load
global.get $sp
i32.const 4
i32.add
i32.load
i32.lt_s
br_if $l1
)
)
;; free stack space
global.get $sp
i32.const 8
i32.add
global.set $sp
)
)
Pour la suite, notre compilateur produira du code WebAssembly à partir de notre langage Oberon0. Nous ne passerons pas par une version intermédiaire en WebAssembly Text, mais nous allons directement générer du code binaire WebAssembly.
Pour ce faire, je vous propose d’utiliser la bibliothèque Python wasm_gen
À faire
Étudiez le module wasm_gen ainsi que sa documentation pour comprendre comment générer du code WebAssembly à partir de Python.