← Blog

WebAssemblyで動くRuby:ruby.wasm、PicoRuby.wasm、Spinel、Roundhouse

私はRuby Koans in the Browserなどでruby.wasmをよく使っています。今年はさらに三つのプロジェクトが加わり、WebAssemblyでRubyを動かすことがずっとおもしろくなりました。どれも違う道を進んでいます。

最初の三つを比べるために、同じ小さなプログラムをそれぞれで動かしました。まとめ買い割引、8.1%の付加価値税、5ラッペン単位の丸めがあるスイスの請求書と、単語の数え上げです。

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の出力はこうです。

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

ruby.wasm:本物のCRuby

ruby.wasmはRuby公式プロジェクトの一部です。WASI向けにコンパイルしたCRubyそのものなので、標準ライブラリや多くのgemを含め、純粋なRubyのコードならほぼ何でも動きます。始めるには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>

請求書のプログラムはそのまま動き、CRubyとまったく同じ出力になります。CRuby 4.0 そのもの だからです。

その代わり、サイズと起動時間がかかります。ruby+stdlib.wasm は31 MBで、CDNからは圧縮して約8.6 MB。私のマシンでは、ページを読み込んでから出力が出るまで1.2〜2.5秒かかりました。制限もいくつかあります。WebAssemblyにはスレッドがないので、スレッドを起動するライブラリには少し工夫が必要です(Kumiの記事でひとつの方法を紹介しています)。また、C拡張を持つgemはインストールするのではなく、rbwasm ツールで .wasm ファイルに組み込む必要があります。

向いている用途: チュートリアルやプレイグラウンド、既存のRubyライブラリをブラウザで動かすこと、読み込み時間より互換性が大切な場面。

PicoRuby.wasm:小さくて速い

HASUMI HitoshiさんのPicoRubyは、Raspberry Pi Picoのようなワンチップマイコン向けの最小のRuby実装です。WebAssembly版はmrubyのVMを使い、@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 は2.1 MB、圧縮すると約0.9 MBで、私のプログラムはページを読み込んでから約80ミリ秒で出力を表示しました。JavaScriptとの橋渡しもよく考えられています。setTimeout、fetch、イベントリスナーはRubyのブロックを受け取って協調的なタスクとして動き、EmscriptenのAsyncifyによるサイズの増加もありません。binding.irb でブレークポイントを置けるChrome DevToolsの拡張機能や、コンポーネントフレームワークのFunicularまであります。

ただし、PicoRubyはRubyのサブセットです。私のプログラムは三か所を変えないと動きませんでした。Struct、Array#sum、Hash#max_by がないからです。小さなクラス、inject、ループに置き換えると、同じ結果になりました。エンドレスメソッドと format はそのまま動きました。

向いている用途: RubyでJavaScriptを置き換えるインタラクティブなページで、標準ライブラリ全体より起動の速さが大切な場面。

Spinel:事前にコンパイルするRuby

SpinelはMatzのRubyコンパイラです。prismでRubyを解析し、プログラム全体の型を推論してCのコードを生成し、システムのCコンパイラでコンパイルします。できあがるのはインタプリタのいらない単独の実行ファイルです。最初のリリースは2026年9月12日で、プロジェクトのベンチマークでは、幾何平均でYJIT付きのCRubyの約8.5倍の速さだと報告されています。

SpinelにはWebAssemblyのターゲットもあります。wasi-sdkを入れておけば、オプションひとつです。

$ spinel invoice.rb -o invoice                        # ネイティブ、421 KB
$ spinel --target=wasm32-wasi invoice.rb -o invoice.wasm   # 963 KB、gzipで322 KB

請求書のプログラムは一か所も変えずにコンパイルでき、ネイティブの実行ファイルも .wasm モジュールも、CRubyとまったく同じ出力になりました。モジュールはNode.js 25の組み込みのWASIサポートで実行しました。全体で0.18秒で、そのほとんどはNodeの起動時間です。ネイティブの実行ファイルは10ミリ秒かからずに終わり、CRubyは0.2秒でした。

これを可能にしている仕組みが、そのまま制限にもなっています。Spinelはコンパイルの時点でプログラム全体を知っている必要があります。eval や method_missing はなく、send と define_method はソースに書かれた名前でしか使えません。WebAssemblyのターゲットでは整数が32ビットになり、FiberとThreadは使えません。また raise/rescue には、WebAssemblyの例外処理に対応したランタイムが必要です。最近のブラウザ、Node.js、wasmtimeは対応しています。

向いている用途: コマンドラインツール、計算の多いコード、小さな単独のプログラム、インタプリタを同梱せずに .wasm モジュールとして動かしたいRubyのロジック。

Roundhouse:仕様としてのRails

Sam RubyさんのRoundhouseは、一段上のレベルで動きます。手を加えていないRailsアプリケーションを読み込み、型注釈も、動いているアプリも、データベースもなしに、プログラム全体の推論で型を割り出します。has_many :comments を見ればアソシエーションがどのクラスを返すかはもうわかっていて、Roundhouseはそれを利用します。

解析結果は三つの使い方があります。

入力はRailsが最初ですが、Railsだけではありません。RodaとSequelをメンテナンスしているJeremy Evansさんと一緒に、Sam RubyさんはRoda + Sequelを二つ目の入力フレームワークとして加えました。小さなRodaのブログがすでに同じパイプラインを通り、変換後も自身のテストに通っています。Hanamiへの対応も議論されています。さらに、生成されたアプリが使うフレームワークのランタイム、つまり独自のActive Record、Action Controller、Action Viewも、それ自体がRubyで書かれていて、同じパイプラインで各ターゲット言語に変換されます。そのテストスイートはCrystal、Kotlin、Swift、TypeScript、Ruby、Spinelで通っています。

WebAssemblyが関わるのはブラウザのIDEです。RoundhouseはRustで書かれていて、解析部分がWebAssemblyにコンパイルされ、Web Workerの中で動きます。自分のディスクにあるRailsアプリを開くこともでき、何もアップロードされません。約1100ファイルのMastodonも数秒で解析され、マウスを重ねると型が表示され、ビューからインスタンス変数をセットしているコントローラのアクションへジャンプでき、リクエストのトレースも見られます。さらに、アプリのTypeScript版をブラウザ用にまとめ、SQLiteをWebAssemblyで動かすターゲットもあります。

向いている用途: Railsのコードベースを理解してチェックすること、そしてRailsアプリがCRubyの外で何になりうるかを探ること。

比較

  ruby.wasm PicoRuby.wasm Spinel Roundhouse
方式 CRubyインタプリタ mrubyベースの小さなVM 事前コンパイラ 解析とトランスパイラ
対応するRuby ほぼすべて サブセット 動的な機能以外すべて Railsアプリ、Roda + Sequelも進行中
私のプログラム そのまま動いた 三か所変更 そのまま動いた —
ダウンロード 圧縮で約8.6 MB 圧縮で約0.9 MB gzipで322 KB(プログラム全体) —
動く場所 ブラウザ、Node.js、WASI ブラウザ ネイティブ、WASI コマンドライン、ブラウザIDE

この四つは、実は競合ではありません。ruby.wasmはRubyの完全な互換性をブラウザに持ち込みます。PicoRuby.wasmはページ上でJavaScriptを置き換えられるほどRubyを軽くします。Spinelは、RubyのプログラムもCのプログラムのようにコンパイルでき、数百キロバイトのWebAssemblyモジュールにまでできることを示しています。そしてRoundhouseは、Railsをはじめとするウェブアプリについて、静的解析でどれだけのことがわかるかを示しています。Ruby開発者にとって、2026年はWebAssemblyのとても良い年です。

上の時間は、ほかの作業もしていた私の開発マシンで測ったものなので、ベンチマークではなくおおまかな印象として読んでください。起動時間をきちんと比べるなら、sue445さんのruby-wasm-startup-benchmarkがruby.wasmとPicoRuby.wasmをブラウザで測っています。

リンク