Rust e WebAssembly
WebAssembly (spesso abbreviato in Wasm) ti permette di eseguire codice scritto in linguaggi come Rust dentro un browser web, accanto a JavaScript. Pensalo come un secondo motore nel browser: JavaScript continua a guidare la pagina, ma quando c’è un lavoro pesante (filtri per immagini, fisica, parsing di file grandi) puoi passarlo a codice Rust compilato e veloce.
In questa pagina scriverai una piccola libreria Rust, la compilerai in Wasm e la chiamerai da una pagina web. Niente di tutto questo gira nel Rust Playground (servono un browser e strumenti in più), quindi la maggior parte degli esempi sono semplici blocchi di codice per il tuo computer. Le parti in puro Rust, però, puoi provarle subito.
Cos’è WebAssembly, e perché Rust ci sta bene
Sezione intitolata “Cos’è WebAssembly, e perché Rust ci sta bene”WebAssembly è un formato binario compatto per i programmi. Tutti i browser principali possono eseguirlo, a una velocità vicina a quella nativa, dentro la stessa sandbox di sicurezza di JavaScript. Non è solo per i browser: anche server e piattaforme edge eseguono Wasm.
Rust è uno dei linguaggi più usati per Wasm, per alcuni motivi:
- Niente garbage collector né grossi runtime. Rust non ha bisogno di portarsi dietro macchinari extra insieme al tuo codice, quindi i file
.wasmpossono essere piccoli. - Ottimi strumenti.
wasm-bindgenscrive per te il codice di collegamento (glue code) tra Rust e JavaScript, ewasm-packcostruisce tutto con un solo comando. - Sicurezza e velocità. Ottieni la stessa sicurezza della memoria e le stesse prestazioni che avresti su un desktop.
Preparare gli strumenti
Sezione intitolata “Preparare gli strumenti”Per prima cosa, di’ a Rust che può compilare per il target Wasm. Un target è «il tipo di macchina su cui girerà il codice»; wasm32-unknown-unknown significa «Wasm a 32 bit, nessun sistema operativo in particolare»:
rustup target add wasm32-unknown-unknownPoi installa wasm-pack. Compila il tuo crate, esegue wasm-bindgen e mette tutto ciò che ti serve in un’unica cartella. Usa l’installer ufficiale, oppure compilalo con Cargo:
# Installer per Linux / macOS dal sito di wasm-packcurl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# oppure, su qualsiasi sistema con Rust installatocargo install wasm-pack --lockedScrivere una libreria Rust per Wasm
Sezione intitolata “Scrivere una libreria Rust per Wasm”Un modulo Wasm è una libreria, non un programma con main. Creane una:
cargo new --lib hello-wasmcd hello-wasmPoi modifica Cargo.toml. La riga crate-type dice a Cargo di produrre una libreria adatta a .wasm (cdylib), e wasm-bindgen è il crate che collega Rust e JavaScript:
[package]name = "hello-wasm"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib", "rlib"]
[dependencies]wasm-bindgen = "0.2"Ora scrivi due funzioni in src/lib.rs. L’attributo #[wasm_bindgen] marca ciascuna come «chiamabile da JavaScript»:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]pub fn add(a: i32, b: i32) -> i32 { a + b}
#[wasm_bindgen]pub fn greet(name: &str) -> String { format!("Hello, {name}! This string was built in Rust.")}Non ti serve altro codice Rust. Nota che sono funzioni normalissime: wasm-bindgen si occupa di trasformare una stringa JavaScript in una &str e la String restituita di nuovo in una stringa JavaScript. (Il saluto resta in inglese perché più avanti vedrai l’output reale che ha prodotto nel browser.)
Testare prima la logica in modo nativo
Sezione intitolata “Testare prima la logica in modo nativo”Ecco un’abitudine che fa risparmiare un sacco di tempo: tieni il lavoro vero in normali funzioni Rust e testale sul tuo computer con cargo test o con un main veloce. Fare debug in un browser è più lento che farlo in un terminale. La stessa logica, senza l’attributo, gira ovunque:
fn add(a: i32, b: i32) -> i32 { a + b}
fn greet(name: &str) -> String { format!("Hello, {name}! This string was built in Rust.")}
fn main() { println!("{}", greet("Ferris")); println!("2 + 3 = {}", add(2, 3));}Output:
Hello, Ferris! This string was built in Rust.2 + 3 = 5Una volta che la logica è giusta, il wrapper #[wasm_bindgen] è solo una porta sottile verso il browser. (Anche i test marcati #[test] girano in modo nativo con cargo test: vedi Test.)
Compilare con wasm-pack
Sezione intitolata “Compilare con wasm-pack”Dalla cartella del crate, esegui:
wasm-pack build --target web--target web significa «crea file che un browser possa caricare direttamente con un tag <script type="module">», senza bisogno di un bundler come webpack. Quando l’abbiamo eseguito, la nuova cartella pkg/ conteneva:
pkg/├── hello_wasm_bg.wasm the compiled Rust code├── hello_wasm.js JavaScript glue that loads it and wraps each function├── hello_wasm.d.ts TypeScript types for your functions├── hello_wasm_bg.wasm.d.ts└── package.jsonIn ordine: il codice Rust compilato, il codice JavaScript di collegamento che lo carica e avvolge ogni funzione, i tipi TypeScript delle tue funzioni. Il trattino di hello-wasm è diventato un trattino basso nei nomi dei file. Di default wasm-pack build fa una build release (ottimizzata); aggiungi --dev per una build più veloce e non ottimizzata mentre sperimenti.
Chiamarlo da JavaScript
Sezione intitolata “Chiamarlo da JavaScript”Crea index.html accanto a Cargo.toml (così pkg/ si trova lì vicino):
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Hello from Rust</title></head><body> <p id="message">Loading…</p>
<script type="module"> import init, { add, greet } from "./pkg/hello_wasm.js";
await init(); // scarica e avvia il file .wasm
const text = greet("Ferris"); const sum = add(2, 3); document.getElementById("message").textContent = `${text} 2 + 3 = ${sum}`; </script></body></html>L’export predefinito, init(), scarica il file .wasm e lo avvia. Dopo di che, add e greet sono normali funzioni JavaScript.
I browser non caricano moduli da un indirizzo file://, quindi ti serve un piccolo server web locale. Se hai Python installato, questo è il modo più rapido:
python3 -m http.server 8080Apri http://localhost:8080 nel browser. Quando l’abbiamo fatto noi, la pagina mostrava:
Hello, Ferris! This string was built in Rust. 2 + 3 = 5Quella frase («Ciao, Ferris! Questa stringa è stata costruita in Rust») l’ha costruita Rust, girando dentro il browser. 🦀
Cosa attraversa il confine a basso costo (e cosa no)
Sezione intitolata “Cosa attraversa il confine a basso costo (e cosa no)”Rust e JavaScript non condividono la memoria allo stesso modo. Il modulo Wasm ha un suo blocco di memoria, e i valori devono viaggiare tra i due lati. Alcuni viaggi sono gratis; altri costano una copia:
| Tipo in Rust | In JavaScript | Costo |
|---|---|---|
i32, u32, f64, bool |
number, boolean |
Basso: passati direttamente |
&str, String |
string |
Una copia, più la conversione da UTF-16 a UTF-8 (o viceversa) |
&[u8], Vec<u8> |
Uint8Array |
Una copia dentro (o fuori da) la memoria Wasm |
&mut [u8] |
Uint8Array |
Copiato dentro, poi ricopiato fuori dopo la chiamata |
Quindi la regola d’oro è: fai poche chiamate che fanno ognuna tanto lavoro. Chiamare Rust una volta per elaborare un’intera immagine è veloce. Chiamarlo un milione di volte, una per pixel, fa passare la maggior parte del tempo ad attraversare il confine.
Ecco come appare. Questa funzione schiarisce un intero buffer di valori dei pixel con una sola chiamata:
use wasm_bindgen::prelude::*;
/// Rende ogni pixel più chiaro. JS passa un Uint8Array.#[wasm_bindgen]pub fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { *p = p.saturating_add(amount); }}Da JavaScript, dopo await init(), chiameresti brighten(pixels, 20) con un Uint8Array, e l’array verrebbe aggiornato sul posto. Il ciclo in sé è puro Rust, quindi puoi testarlo in modo nativo:
fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { // saturating_add si ferma a 255 invece di andare in overflow *p = p.saturating_add(amount); }}
fn main() { let mut pixels = [10, 100, 250]; brighten(&mut pixels, 20); println!("{pixels:?}");}Output:
[30, 120, 255]Nota che 250 + 20 è diventato 255, non un crash né un giro dei valori: saturating_add si ferma al massimo. È esattamente quello che vuoi per i colori.
Tenere piccolo il file .wasm
Sezione intitolata “Tenere piccolo il file .wasm”Ogni byte del tuo file .wasm va scaricato prima che la pagina possa usarlo. Qualche consiglio:
- Distribuisci sempre la build release. Per il nostro piccolo esempio,
wasm-pack build --devha prodotto un file.wasmdi 58.591 byte; la build release predefinita era di 21.530 byte. (Sono numeri della nostra macchina conwasm-optdisattivato: i tuoi saranno diversi.) - Lascia che wasm-pack esegua
wasm-opt. In modalità release, wasm-pack esegue di default l’ottimizzatorewasm-optdi Binaryen sull’output, e di solito lo riduce ancora. - Attiva la compressione sul tuo server. Il nostro file da 21.530 byte pesava 8.464 byte dopo gzip.
- Prova le impostazioni pensate per la dimensione, poi misura:
[profile.release]opt-level = "z" # ottimizza per la dimensione ("s" è una versione più blanda)lto = truecodegen-units = 1Sii onesto con te stesso e misura. Per il nostro esempio con due funzioni, queste impostazioni non hanno aiutato: il file è venuto di 22.817 byte, un po’ più grande. Di solito ripagano nei crate più grandi. Anche cose come format! e i messaggi di panic si portano dietro codice, quindi un crate che li evita può essere molto più piccolo.
Dove si usa Wasm nel mondo reale
Sezione intitolata “Dove si usa Wasm nel mondo reale”Alcuni esempi, ciascuno con il resoconto scritto dall’azienda stessa (in inglese):
- Figma compila il suo editor di design in C++ in WebAssembly e ha riferito di aver ridotto di 3 volte il tempo di caricamento. (Non è Rust, ma mostra cosa può fare Wasm in un’app grande.)
- Cloudflare Workers supporta Rust tramite il crate
workers-rs: il tuo codice Rust viene compilato in Wasm e gira sui server di Cloudflare in tutto il mondo. - Le Shopify Functions sono moduli WebAssembly che personalizzano i negozi, e Shopify consiglia Rust come il linguaggio più performante per scriverle.
- L’estensione per browser di 1Password ha spostato in WebAssembly la compilazione delle pagine e ha riferito che gira almeno due volte più veloce.
Errori comuni
Sezione intitolata “Errori comuni”Dimenticare crate-type = ["cdylib"]. Senza, wasm-pack si ferma subito. Per fortuna l’errore ti dice esattamente cosa aggiungere:
Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:
[lib]crate-type = ["cdylib", "rlib"]Il messaggio dice che il tipo di crate deve essere cdylib per compilare in WebAssembly, e ti mostra le righe da aggiungere a Cargo.toml.
Aprire index.html con un doppio clic. La pagina resta su «Loading…», e la console del browser (in Chrome) dice:
Access to script at 'file:///…/pkg/hello_wasm.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.Cioè: la policy CORS ha bloccato lo script, perché le richieste cross-origin funzionano solo con protocolli come http e https. La soluzione: servi la cartella con un server web locale, come python3 -m http.server, e apri invece l’indirizzo http://localhost.
Chiamare una funzione prima di await init(). Il modulo Wasm non è ancora caricato, quindi il codice di collegamento non ha niente da chiamare. Quando abbiamo tolto la riga init(), Chrome ha mostrato Cannot read properties of undefined (reading '__wbindgen_free'). Fai sempre prima await init().
Prossimi passi: app web complete in Rust
Sezione intitolata “Prossimi passi: app web complete in Rust”Chiamare qualche funzione Rust da JavaScript è un ottimo inizio. Se vuoi costruire l’intera app web in Rust, dai un’occhiata a questi framework. Entrambi compilano in Wasm e ti permettono di scrivere componenti in Rust, un po’ come con React:
- Leptos (in inglese): un framework moderno e veloce che può anche generare le pagine sul server.
- Yew (in inglese): un framework maturo basato su componenti.
Riepilogo
Sezione intitolata “Riepilogo”- WebAssembly è un formato binario veloce e sicuro che i browser (e i server) possono eseguire; Rust ci si compila bene perché non ha un garbage collector.
- Aggiungi il target con
rustup target add wasm32-unknown-unknowne installawasm-pack. - Marca le funzioni con
#[wasm_bindgen], impostacrate-type = ["cdylib", "rlib"]e compila conwasm-pack build --target web. - In JavaScript, prima
await init(), poi chiama le tue funzioni; servi la pagina viahttp://, nonfile://. - I numeri attraversano il confine a basso costo; stringhe e slice vengono copiate, quindi fai poche chiamate corpose.
- Distribuisci build release con compressione, e misura prima di fidarti dei ritocchi sulla dimensione.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quale comando aggiunge il target Rust per WebAssembly nel browser?
2. Cosa fa l'attributo #[wasm_bindgen]?
3. La tua pagina resta su «Loading…» e la console mostra un errore CORS per un indirizzo file:///. Come lo risolvi?
4. Quale di questi è il più economico da passare da JavaScript a Rust?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Compiling from Rust to WebAssembly — MDN (in inglese): un tutorial passo per passo che costruisce ed esegue un progetto wasm-pack.
- The wasm-bindgen Guide (in inglese): tutti i tipi supportati, come chiamare le API del browser e tanti esempi.
- Documentazione di wasm-pack (in inglese): tutte le opzioni di
build, i target e i profili. - wasm32-unknown-unknown — The rustc book (in inglese): cosa supporta questo target e i suoi limiti.
- WebAssembly su MDN (in inglese): cos’è Wasm e come lo eseguono i browser, indipendentemente da Rust.