← Articles

Ruby en WebAssembly : ruby.wasm, PicoRuby.wasm, Spinel et Roundhouse

J’utilise beaucoup ruby.wasm, par exemple pour les Ruby Koans dans le navigateur. Cette année, trois autres projets ont rendu Ruby en WebAssembly bien plus intéressant, et chacun emprunte une voie différente :

Pour comparer les trois premiers, j’ai exécuté le même petit programme dans chacun d’eux : une facture suisse avec rabais de quantité, TVA à 8,1 % et arrondi aux 5 centimes, plus un comptage de mots.

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 affiche :

Subtotal     208.00
MWST 8.1%     16.85
Total        224.85
["the", 3]

ruby.wasm : le véritable CRuby

ruby.wasm fait partie du projet Ruby officiel. C’est CRuby lui-même, compilé pour WASI ; il exécute donc presque tout code Ruby pur, y compris la bibliothèque standard et de nombreux gems. Deux balises script suffisent pour démarrer :

<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>

Le programme de facture tourne sans modification et donne exactement la sortie de CRuby, puisque c’est CRuby 4.0.

Le prix à payer, c’est la taille et le temps de démarrage. ruby+stdlib.wasm pèse 31 MB, environ 8,6 MB compressés via le CDN, et sur ma machine la sortie est apparue 1,2 à 2,5 secondes après le chargement de la page. Il y a aussi quelques limites : WebAssembly n’a pas de threads, donc les bibliothèques qui démarrent un thread nécessitent un petit contournement (j’en décris un pour Kumi), et les extensions C doivent être compilées dans le fichier .wasm avec l’outil rbwasm au lieu d’être installées.

Idéal pour : tutoriels et bacs à sable, exécution de bibliothèques Ruby existantes dans le navigateur, tout ce où la compatibilité compte plus que le temps de chargement.

PicoRuby.wasm : petit et rapide

PicoRuby de Hitoshi Hasumi est la plus petite implémentation de Ruby, conçue pour des microcontrôleurs monopuces comme le Raspberry Pi Pico. Sa version WebAssembly utilise la VM mruby et est publiée sous le nom @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 pèse 2,1 MB, soit environ 0,9 MB compressé, et mon programme a affiché sa sortie environ 80 millisecondes après le chargement de la page. Le pont JavaScript est bien conçu : setTimeout, fetch et les écouteurs d’événements acceptent des blocs Ruby et s’exécutent comme des tâches coopératives, sans le surcoût en taille de l’Asyncify d’Emscripten. Il existe même une extension Chrome DevTools avec des points d’arrêt binding.irb, ainsi qu’un framework de composants, Funicular.

PicoRuby n’est toutefois qu’un sous-ensemble de Ruby. Mon programme a nécessité trois modifications avant de tourner : il n’y a ni Struct, ni Array#sum, ni Hash#max_by. Avec une petite classe, inject et une boucle à la place, les résultats étaient identiques. Les méthodes sans end et format ont fonctionné d’emblée.

Idéal pour : les pages interactives où Ruby remplace JavaScript, et où un démarrage rapide compte plus que la bibliothèque standard complète.

Spinel : Ruby compilé à l’avance

Spinel est le compilateur Ruby de Matz. Il analyse Ruby avec prism, infère les types de l’ensemble du programme, génère du C et le compile avec le compilateur C du système. Le résultat est un binaire autonome, sans interpréteur. La première version est sortie le 12 septembre 2026, et le projet annonce sur ses benchmarks une moyenne géométrique d’environ 8,5 fois la vitesse de CRuby avec YJIT.

Spinel dispose aussi d’une cible WebAssembly. Une fois le wasi-sdk installé, il suffit d’une option :

$ spinel invoice.rb -o invoice                        # natif, 421 KB
$ spinel --target=wasm32-wasi invoice.rb -o invoice.wasm   # 963 KB, 322 KB avec gzip

Le programme de facture a compilé sans la moindre modification, et le binaire natif comme le module .wasm ont affiché exactement la même chose que CRuby. J’ai exécuté le module avec Node.js 25 et son support WASI intégré. L’exécution complète a pris 0,18 seconde, surtout pour le démarrage de Node. Le binaire natif a terminé en moins de 10 millisecondes, là où CRuby a eu besoin de 0,2 seconde.

Ce qui rend cela possible en est aussi la limite : Spinel doit connaître tout le programme au moment de la compilation. Il n’y a ni eval ni method_missing, et send et define_method ne fonctionnent qu’avec des noms écrits dans le code source. Sur la cible WebAssembly, les entiers sont sur 32 bits, il n’y a ni fibers ni threads, et raise/rescue nécessite un runtime gérant les exceptions WebAssembly, ce que font les navigateurs actuels, Node.js et wasmtime.

Idéal pour : les outils en ligne de commande, le code gourmand en calcul, les petits programmes autonomes, et la logique Ruby qui doit tourner comme module .wasm sans embarquer d’interpréteur.

Roundhouse : Rails comme spécification

Roundhouse de Sam Ruby travaille un niveau plus haut. Il lit une application Rails non modifiée et en reconstitue les types par inférence sur l’ensemble du programme, sans annotations de types, sans application en cours d’exécution ni base de données. has_many :comments indique déjà quelle classe une association renvoie, et Roundhouse s’en sert.

L’analyse sert de trois manières :

Rails est la première source, mais pas la seule. Avec Jeremy Evans, mainteneur de Roda et de Sequel, Sam Ruby a ajouté Roda + Sequel comme second framework source : un petit blog Roda passe déjà par le même pipeline et réussit ensuite ses propres tests. Un front-end Hanami est en discussion. Et le runtime de framework qu’utilisent les applications générées, leurs propres Active Record, Action Controller et Action View, est lui-même écrit en Ruby et traduit dans chaque langage cible par le même pipeline. Sa suite de tests passe en Crystal, Kotlin, Swift, TypeScript, Ruby et Spinel.

La partie WebAssembly, c’est l’IDE dans le navigateur. Roundhouse est écrit en Rust ; l’analyseur est compilé en WebAssembly et tourne dans un Web Worker. Vous pouvez ouvrir une application Rails depuis votre propre disque, sans que rien ne soit téléversé. Mastodon, avec environ 1100 fichiers, est analysé en quelques secondes ; vous obtenez ensuite les types au survol, la navigation d’une vue vers l’action de contrôleur qui définit ses variables d’instance, et des traces de requêtes. Il existe aussi une cible qui empaquette la version TypeScript d’une application pour l’exécuter dans le navigateur, avec SQLite compilé en WebAssembly.

Idéal pour : comprendre et vérifier une base de code Rails, et explorer ce qu’une application Rails pourrait devenir en dehors de CRuby.

Comparaison

  ruby.wasm PicoRuby.wasm Spinel Roundhouse
Approche interpréteur CRuby petite VM basée sur mruby compilateur anticipé analyseur et transpileur
Ruby couvert presque tout un sous-ensemble tout sauf les fonctions dynamiques applications Rails, Roda + Sequel en cours
Mon programme sans modification trois modifications sans modification —
Téléchargement ~8,6 MB compressé ~0,9 MB compressé 322 KB avec gzip (programme entier) —
Tourne dans navigateur, Node.js, WASI navigateur natif, WASI CLI, IDE dans le navigateur

Ces quatre projets ne sont pas vraiment concurrents. ruby.wasm apporte toute la compatibilité de Ruby dans le navigateur. PicoRuby.wasm rend Ruby assez léger pour remplacer JavaScript sur une page. Spinel montre que des programmes Ruby peuvent être compilés comme des programmes C, jusqu’à un module WebAssembly de quelques centaines de kilo-octets. Et Roundhouse montre tout ce qu’une analyse statique peut savoir d’une application web, à commencer par Rails. Pour un développeur Ruby, 2026 est une très bonne année pour WebAssembly.

Les mesures ci-dessus proviennent de ma machine de développement, occupée à d’autres tâches à ce moment-là ; prenez-les comme des impressions approximatives et non comme un benchmark. Pour une vraie comparaison des temps de démarrage, le ruby-wasm-startup-benchmark de sue445 mesure ruby.wasm et PicoRuby.wasm dans le navigateur.

Liens