emruby: ブラウザで動くMRI

こんにちは、フルタイムRubyコミッタの遠藤です。

Ruby 3.0が出てもう4ヶ月経ってしまいました。最近のTypeProfの開発ですが、vscode拡張として使えるようにするために、一生懸命Language Server Protocolをいじって遊んでるところです。

こっちのほうはまだ実験段階なので、まとまったころに説明するとして、今回は、Ruby 3.0リリース後にほそぼそとやっていたemrubyをご紹介してみます。

emrubyとは

ブラウザの上で動くMRI(Matz Ruby Interpreter)です。

「エムルビー」だと組み込み向けRuby実装の mruby と紛らわしいので、「イーエムルビー」と読んでください(とmatzに言われています)。

デモ

このページを開いてみてください。

mame.github.io

"Code"の下のエディタ部分にRubyコードを書き、Runのボタンを押すと、実行結果が"Result"の下に出てきます。

f:id:ku-ma-me:20210430180447p:plain

たとえば

p 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10

と書いて実行すると 55 と出てきます。

require "stringio"
p StringIO.new("foobar").read(3)

のように(一部の)拡張ライブラリも使えます。

JavaScriptでは考えにくいですが、同期的なウェイトも可能です。

puts "waiting..."
sleep 1
puts "done"

とても実験的ですが、JavaScriptコードを呼び出すことも可能です。

p emscripten_run_script_int(<<JAVASCRIPT)
(function() {
  var sum = 0;
  for (var i = 1; i <= 100; i++) sum += i;
  return sum;
})();
JAVASCRIPT

JavaScriptで1から100まで足して、その結果をRubyのKernel#pメソッドで出力します(いまのところ、intの返り値しか対応してません)。

技術的な話

原理的には、C言語ソースコードをWebAssemblyにコンパイルしてくれるEmscriptenを使ってMRIをコンパイルしただけです。

しかし現実的には、コンパイルだけでも細々とした修正や対応が必要でした。

  • Emscriptenでは使えないC APIをいろいろケアした(たとえばIO.popenはNotImplementedErrorにしたとか)
  • 保守的GCがそのままでは動かないので、Emscriptenが提供している保守的GC用のAPIを使うようにした *1
  • 動的リンクは実験的サポートらしかったので、必要な拡張ライブラリを静的リンクするようにした
  • その他こまごまとコンパイルオプションを調整した

これらの変更はRubyソースコードへのパッチにする必要がありますが、幸いコミット権限を持っているので、すでにmasterを更新してあります。パッチを管理しなくてすんでよかった。

コンパイルしてみたい人は、emscriptenを使えるようにして(Emscriptenのドキュメント参照)、Rubyのmasterブランチを次のようにコンパイルすると ruby.wasm などができるはずです。

$ ./configure \
  --build x86_64-pc-linux-gnu \
  --host wasm32-unknown-emscripten \
  --with-static-linked-ext \
  --with-ext=ripper,date,strscan,io/console,…,psych \
  optflags=-Os debugflags=-g0 \
  CC=emcc LD=emcc AR=emar RANLIB=emranlib

$ make

フロントエンド、といってもemrubyは全部フロントエンドですが、特にユーザインターフェイスの部分はNext.jsxterm.jscodemirrorで自作しています。↓のHackarade(社内ハッカソン)に乗っかってエイヤと作りました。

techlife.cookpad.com

想定問答

なんで作ったの?

RubyがWebフロントエンド市場に本格的に進出する足がかり!という意気込みが当初は少しだけありました。実際、WASMが話題になった2017年ごろは、ブラウザでも適材適所で言語を選べるのでは、という期待感があったと思います。が、最近のTypeScriptの隆盛を見ると、なかなかそういう感じにはなってないですね。やっぱりJavaScriptは強い。

しかしそれでも、RustやGoやKotlinなど、最近の言語はWASM対応をうたっていることが多いです。どの言語がどのマーケットをとるかは運ですが、動いてない言語は候補にもならないので、動くようにしておくことは大切かなと思っています。宝くじを買う気分。*2

などと適当なことをいいましたが、正直に言えばJust for funなところが大きかったです。ブラウザの上でRubyが動くのはそれだけで楽しい。

完成度は?

rubygemsやirbも含めて一応動いています。意外と動くなあ、という感じです。

https://mame.github.io/emruby/irb/

とはいえ、やはりEmscriptenは普通の環境ではないので、普通の環境のMRIに比べるといろいろ問題があります。たとえば、Fiberが動かない*3、Threadも動かない、など。もし興味のある人がいたら一緒に改良しましょう。

(ちなみにirbのデモではSharedArrayBufferを使っているのですが、これはChrome 91から制限が強化されるらしいので、Chrome 90以前でしか動かない見込みです。Chrome 91は5月にリリースされるらしいので、つまり、今しか動きません……)

なお、もし今すぐブラウザの上で動くRubyを書きたいなら、Opalを検討するのがよいと思います。他には、次の記事も参考になると思います。

blog.unasuke.com

まとめ

ブラウザの上で動くMRI、emrubyを紹介しました。遊んでもらえるとうれしいです。

先日銀座Rails#32でもemrubyについて話したので、発表資料を貼っておきます。Rubyのビルドプロセスや、Emscripten自体に興味がある人は面白いと思います。

www.slideshare.net

*1:マシンスタックやレジスタを走査してオブジェクトの参照を探すということをするのですが、そのためにスタックの先頭と終端を得るAPI emscripten_scan_stack やレジスタをスキャンするためのAPI emscripten_scan_registers がEmscriptenによって提供されていたので、それらを使うようにしました。

*2:似たような気持ちで、AndroidエミュレータでもRubyをCIテストしてたりします。こっちは意外と全テストが完走するのですごい。だれかiOSもやらないかな。

*3:emscripten_fiber_init や emscripten_fiber_swap などのAPIを使っているので、動くはずなのですが、minirubyでは動くけどrubyでは動かない状態です。原因もよくわからないので、デバッグが必要。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/