Aller au contenu

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 .wasm avec la commande wat2wasm du toolkit wabt.
  • Validez le fichier .wasm avec la commande wasm-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 .wasm pour 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

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.

Le Zuse Z4

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

La HP 35 de Hewlett Packard

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.

Memory

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 $sp
  • y à l’adresse $sp + 4
  • z à l’adresse $sp + 8

Variables locales

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.wat pour additionner deux nombres fournis par le runtime
  • Compilez le fichier .wat en 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.wat pour afficher les nombres impairs
  • Compilez le fichier .wat en 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.