Ruby in WebAssembly: ruby.wasm, PicoRuby.wasm, Spinel e Roundhouse
Uso molto ruby.wasm, per esempio per i Ruby Koans nel browser. Quest’anno altri tre progetti hanno reso Ruby in WebAssembly molto più interessante, e ognuno segue una strada diversa:
- ruby.wasm compila in WebAssembly il vero interprete CRuby.
- PicoRuby.wasm compila un piccolo Ruby pensato per i microcontrollori.
- Spinel, il nuovo compilatore ahead-of-time di Matz, trasforma un programma Ruby in C e da lì in un binario nativo o in un modulo
.wasm. - Roundhouse, di Sam Ruby, legge intere applicazioni web, a cominciare da Rails, ed esegue la sua analisi nel browser come WebAssembly.
Per confrontare i primi tre ho eseguito in ciascuno lo stesso piccolo programma: una fattura svizzera con sconto quantità, IVA all’8,1% e arrotondamento ai 5 centesimi, più un conteggio di parole.
Item = Struct.new(:name, :quantity, :unit_price)
class Invoice
VAT = 0.081
def initialize(items)
@items = items
end
def line_total(item)
gross = item.quantity * item.unit_price
item.quantity >= 10 ? gross * 0.9 : gross
end
def subtotal = @items.sum { |item| line_total(item) }
def vat = subtotal * VAT
def total = ((subtotal + vat) * 20).round / 20.0
end
invoice = Invoice.new([
Item.new("Beratung", 2, 50.0),
Item.new("Kaffee", 12, 10.0)
])
[["Subtotal", invoice.subtotal], ["MWST 8.1%", invoice.vat], ["Total", invoice.total]].each do |label, amount|
puts format("%-10s %8.2f", label, amount)
end
counts = Hash.new(0)
"the quick brown fox jumps over the lazy dog the end".split.each { |w| counts[w] += 1 }
p counts.max_by { |_, n| n }
CRuby stampa:
Subtotal 208.00
MWST 8.1% 16.85
Total 224.85
["the", 3]
ruby.wasm: il vero CRuby
ruby.wasm fa parte del progetto Ruby ufficiale. È CRuby stesso, compilato per WASI, quindi esegue quasi tutto il codice in puro Ruby, compresa la libreria standard e molte gem. Per iniziare bastano due tag script:
<script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
<script type="text/ruby">
require "js"
JS.global[:document].title = "Hello from #{RUBY_VERSION}"
</script>
Il programma della fattura gira senza modifiche e produce esattamente l’output di CRuby, perché è CRuby 4.0.
Il prezzo sono dimensioni e tempo di avvio. ruby+stdlib.wasm pesa 31 MB, circa 8,6 MB compressi via CDN, e sulla mia macchina l’output compariva da 1,2 a 2,5 secondi dopo il caricamento della pagina. Ci sono anche alcuni limiti: WebAssembly non ha thread, quindi le librerie che avviano un thread richiedono un piccolo accorgimento (ne descrivo uno per Kumi), e le estensioni C vanno compilate nel file .wasm con lo strumento rbwasm invece di essere installate.
Ideale per: tutorial e playground, eseguire librerie Ruby esistenti nel browser, tutto ciò in cui la compatibilità conta più del tempo di caricamento.
PicoRuby.wasm: piccolo e veloce
PicoRuby di Hitoshi Hasumi è la più piccola implementazione di Ruby, pensata per microcontrollori a chip singolo come il Raspberry Pi Pico. La sua build WebAssembly usa la VM di mruby ed è pubblicata come @picoruby/wasm-wasi:
<script src="https://cdn.jsdelivr.net/npm/@picoruby/wasm-wasi@latest/dist/init.iife.js"></script>
<script type="text/ruby">
require "js"
button = JS.document.getElementById("btn")
button.addEventListener("click") do |event|
puts "clicked at #{event[:clientX]}, #{event[:clientY]}"
end
</script>
picoruby.wasm pesa 2,1 MB, circa 0,9 MB compressi, e il mio programma ha stampato l’output circa 80 millisecondi dopo il caricamento della pagina. Il ponte con JavaScript è ben progettato: setTimeout, fetch e gli event listener accettano blocchi Ruby e girano come task cooperativi, senza il costo in dimensioni dell’Asyncify di Emscripten. C’è perfino un’estensione per Chrome DevTools con breakpoint binding.irb e un framework a componenti, Funicular.
PicoRuby però è un sottoinsieme di Ruby. Il mio programma ha richiesto tre modifiche prima di girare: mancano Struct, Array#sum e Hash#max_by. Sostituendoli con una piccola classe, inject e un ciclo, i risultati erano gli stessi. Gli endless method e format hanno funzionato subito.
Ideale per: pagine interattive in cui Ruby sostituisce JavaScript e in cui un avvio rapido conta più della libreria standard completa.
Spinel: Ruby compilato in anticipo
Spinel è il compilatore Ruby di Matz. Analizza Ruby con prism, inferisce i tipi dell’intero programma, genera C e lo compila con il compilatore C di sistema. Il risultato è un binario autonomo senza interprete. La prima release è uscita il 12 settembre 2026 e sui suoi benchmark il progetto indica una media geometrica di circa 8,5 volte la velocità di CRuby con YJIT.
Spinel ha anche un target WebAssembly. Con il wasi-sdk installato basta un flag:
$ spinel invoice.rb -o invoice # nativo, 421 KB
$ spinel --target=wasm32-wasi invoice.rb -o invoice.wasm # 963 KB, 322 KB con gzip
Il programma della fattura è stato compilato senza alcuna modifica, e sia il binario nativo sia il modulo .wasm hanno stampato esattamente ciò che stampava CRuby. Ho eseguito il modulo con Node.js 25 e il suo supporto WASI integrato. L’intera esecuzione ha richiesto 0,18 secondi, in gran parte per l’avvio di Node. Il binario nativo ha terminato in meno di 10 millisecondi, contro 0,2 secondi di CRuby.
Ciò che lo rende possibile è anche il suo limite: Spinel deve conoscere l’intero programma in fase di compilazione. Non ci sono eval né method_missing, e send e define_method funzionano solo con nomi scritti nel sorgente. Sul target WebAssembly gli interi sono a 32 bit, non ci sono fiber né thread, e raise/rescue richiede un runtime con la gestione delle eccezioni WebAssembly, che i browser attuali, Node.js e wasmtime offrono.
Ideale per: strumenti da riga di comando, codice ad alta intensità di calcolo, piccoli programmi autonomi e logica Ruby da eseguire come modulo .wasm senza distribuire un interprete.
Roundhouse: Rails come specifica
Roundhouse di Sam Ruby lavora un livello più in alto. Legge un’applicazione Rails non modificata e ne ricava i tipi tramite inferenza sull’intero programma, senza annotazioni di tipo, senza un’app in esecuzione e senza database. has_many :comments dice già quale classe restituisce un’associazione, e Roundhouse lo sfrutta.
L’analisi viene usata in tre modi:
- Verifica: errori di tipo, sicurezza rispetto a nil e query N+1, da riga di comando, come plugin per l’editor (LSP) o per un agente AI (MCP).
- Transpilazione: l’app come progetto autonomo in Rust, Go, TypeScript, Crystal, Elixir, Kotlin, Swift, Python, C# o Ruby semplice. Ogni target viene testato pagina per pagina contro la vera app Rails.
- Compilazione: con Spinel l’app diventa un unico binario nativo. Campfire, l’app di chat di Basecamp, gira così e supera la propria suite di test.
Rails è il primo input, ma non l’unico. Insieme a Jeremy Evans, che mantiene Roda e Sequel, Sam Ruby ha aggiunto Roda + Sequel come secondo framework sorgente: un piccolo blog Roda attraversa già la stessa pipeline e supera poi i propri test. Si sta discutendo un front end per Hanami. E il runtime del framework usato dalle app generate, con i propri Active Record, Action Controller e Action View, è a sua volta scritto in Ruby e tradotto in ogni linguaggio target dalla stessa pipeline. La sua suite di test passa in Crystal, Kotlin, Swift, TypeScript, Ruby e Spinel.
La parte WebAssembly è l’IDE nel browser. Roundhouse è scritto in Rust, e l’analizzatore è compilato in WebAssembly e gira in un Web Worker. Si può aprire un’app Rails dal proprio disco senza caricare nulla. Mastodon, con circa 1’100 file, viene analizzato in pochi secondi, dopodiché si hanno i tipi al passaggio del mouse, il go-to-definition da una view all’azione del controller che ne imposta le variabili d’istanza e le tracce delle richieste. C’è anche un target che impacchetta la versione TypeScript di un’app per farla girare nel browser, con SQLite compilato in WebAssembly.
Ideale per: capire e verificare una codebase Rails ed esplorare cosa potrebbe diventare un’app Rails al di fuori di CRuby.
A confronto
| ruby.wasm | PicoRuby.wasm | Spinel | Roundhouse | |
|---|---|---|---|---|
| Approccio | interprete CRuby | piccola VM basata su mruby | compilatore ahead-of-time | analizzatore e transpiler |
| Ruby supportato | quasi tutto | un sottoinsieme | tutto tranne le funzionalità dinamiche | app Rails, Roda + Sequel avviato |
| Il mio programma | girato senza modifiche | tre modifiche | girato senza modifiche | — |
| Download | ~8,6 MB compressi | ~0,9 MB compressi | 322 KB con gzip (programma intero) | — |
| Gira in | browser, Node.js, WASI | browser | nativo, WASI | CLI, IDE nel browser |
Questi quattro non sono veri concorrenti. ruby.wasm porta nel browser la piena compatibilità di Ruby. PicoRuby.wasm rende Ruby abbastanza leggero da sostituire JavaScript in una pagina. Spinel dimostra che i programmi Ruby si possono compilare come programmi C, fino a un modulo WebAssembly di poche centinaia di kilobyte. E Roundhouse mostra quanto un’analisi statica possa sapere di un’app web, a cominciare da Rails. Per uno sviluppatore Ruby, il 2026 è un anno ottimo per WebAssembly.
I tempi indicati provengono dalla mia macchina di sviluppo, che in quel momento era occupata con altro: vanno quindi letti come impressioni approssimative e non come benchmark. Per un confronto serio dei tempi di avvio, il ruby-wasm-startup-benchmark di sue445 misura ruby.wasm e PicoRuby.wasm nel browser.
Link
- ruby.wasm e la sua documentazione
- PicoRuby e PicoRuby.wasm su npm
- Spinel e le sue note su WebAssembly
- Roundhouse, l’IDE nel browser e l’articolo di Sam Ruby Campfire Chats