Come Funziona WebAssembly
Architettura di WebAssembly
WebAssembly è una stack machine: le istruzioni leggono e scrivono valori da uno stack implicito anziché usare registri nominati come le CPU tradizionali.
Stack Machine
Ogni istruzione consuma operandi dalla cima dello stack e vi deposita il risultato.
;; Calcola (10 + 20) * 3
i32.const 10 ;; stack: [10]
i32.const 20 ;; stack: [10, 20]
i32.add ;; stack: [30]
i32.const 3 ;; stack: [30, 3]
i32.mul ;; stack: [90]
Moduli
L’unità fondamentale di Wasm è il modulo. Un modulo può contenere:
- Funzioni (
func): il codice eseguibile. - Memoria lineare (
memory): un array di byte contiguo. - Table (
table): un array di riferimenti a funzioni (usato per i puntatori a funzione e il dynamic dispatch). - Globali (
global): variabili globali mutabili o immutabili. - Import/Export: interfaccia con il mondo esterno (JavaScript, host).
(module
;; Importa una funzione dall'host
(import "env" "log" (func $log (param i32)))
;; Dichiara 1 pagina di memoria (64 KB)
(memory (export "mem") 1)
;; Tabella di funzioni con 2 slot
(table 2 funcref)
;; Variabile globale mutabile
(global $counter (mut i32) (i32.const 0))
;; Funzione esportata
(func (export "increment") (result i32)
global.get $counter
i32.const 1
i32.add
global.set $counter
global.get $counter)
)
Memoria Lineare
La memoria di Wasm è un singolo blocco contiguo di byte (linear memory), accessibile tramite indici interi.
Cresce in pagine da 64 KB. JavaScript e Wasm condividono la stessa vista sulla memoria tramite ArrayBuffer.
// Creare memoria condivisa
const memory = new WebAssembly.Memory({ initial: 1, maximum: 10 });
const view = new Uint8Array(memory.buffer);
view[0] = 42; // Scrivere un byte che Wasm può leggere
Il Ciclo di Vita
Il percorso dal codice sorgente all’esecuzione segue queste fasi:
- Codice sorgente: scrivi in C, Rust, Go, AssemblyScript, ecc.
- Compilazione: il compilatore produce un file
.wasm(bytecode binario). - Fetch: il browser scarica il file
.wasmvia rete. - Compilazione JIT: il motore del browser compila il bytecode in codice macchina nativo.
- Istanziazione: viene creata un’istanza del modulo con le import risolte.
- Esecuzione: le funzioni esportate sono chiamabili da JavaScript.
// Ciclo completo in JavaScript
async function loadWasm() {
// 1. Fetch del modulo
const response = await fetch("calculator.wasm");
// 2. Compilazione + istanziazione in un solo passo
const { instance } = await WebAssembly.instantiateStreaming(response, {
env: { log: (val) => console.log("Wasm dice:", val) },
});
// 3. Esecuzione
const result = instance.exports.add(5, 3);
console.log(result); // 8
}
Differenze Chiave con JavaScript
| Aspetto | JavaScript | WebAssembly |
|---|---|---|
| Tipizzazione | Dinamica | Statica (i32, i64, f32, f64) |
| Parsing | Testo → AST → bytecode | Binario, decodifica molto veloce |
| Memoria | Garbage Collected | Manuale (linear memory) |
| DOM | Accesso diretto | Solo tramite bridge JS |
| Startup | Più lento (parsing + compilazione) | Più veloce (formato compatto) |
| Ottimizzazione | JIT con deoptimizzazione possibile | Ahead-of-time, prestazioni prevedibili |
Istruzioni Principali
Le istruzioni Wasm si dividono in categorie:
;; Aritmetica
i32.add i32.sub i32.mul i32.div_s
;; Confronto
i32.eq i32.lt_s i32.gt_u i32.eqz
;; Memoria
i32.load i32.store memory.grow memory.size
;; Controllo di flusso
block loop br br_if if/else
;; Conversione
i32.wrap_i64 i64.extend_i32_s f32.convert_i32_s
Il flusso di controllo usa blocchi strutturati (niente goto), rendendo Wasm verificabile staticamente e sicuro per l’esecuzione in sandbox.