jsg: ruby.wasmプロジェクトのための小さなヘルパー
ruby.wasmを使うと、ブラウザの中でCRubyを動かせます。RubyのコードとJavaScriptの世界をつなぐのはjs gemです。ただ、プロジェクトを始めるまでには、HTMLページ、ブラウザ用スクリプト、Gemfile、ビルドコマンド、ローカルのWebサーバーと、手作業がいくつか必要になります。
2024年に、この手順をjsgという小さなgemにまとめました。gemspecの説明は一文だけで、「ruby.wasmプロジェクトのセットアップを助け、より書きやすい構文を提供する」というものです。つまり中身は二つあります。プロジェクトを作ってビルドするコマンドラインツールと、ブラウザで読み込むRubyファイルです。
最初に正直にお伝えしておくと、jsgはまだ初期の実験段階です。現在のバージョンは2024年5月の0.1.2で、GitHubのREADMEはBundlerが生成したひな形のままですし、ちゃんとしたテストもまだありません。今のところ、この記事がいちばんドキュメントに近いものです。
インストール
Ruby 3.0以降が必要で、js gem(~> 2.6)とerbに依存しています。
gem install jsg
コマンドラインツール
gemにはjsgコマンドが含まれていて、サブコマンドは三つです。
jsg new myapp # create a project from the template, then build it
jsg build # bundle install, then build assets/ruby-app.wasm
jsg server # serve the current directory with WEBrick
jsg new myappを実行すると、テンプレートが新しいmyappフォルダにコピーされます。中身は次のとおりです。
index.html:ERBテンプレートから生成され、プロジェクト名がタイトルと見出しに入りますsrc/main.rb:自分のコードを書く場所ですassets/browser.script.iife.js:ruby.wasmのブラウザ用スクリプトで、assets/ruby-app.wasmを読み込むように設定されていますruby_wasm、js、jsgを含むGemfilebuild/とrubies/フォルダを除外する.gitignore
コピーが終わると、そのフォルダに移動してそのままビルドを実行します。jsg buildがやることは二つだけで、bundle installとrbwasm build -o assets/ruby-app.wasmです。テンプレートのsrc/main.rbのコメントにあるとおり、自分のコードはここに書き、jsg buildのあとで.wasmファイルの中に入ります。
jsg server(またはjsg serve)はWEBrickをポート8080で起動し、8080が使用中なら次のポートを試します。examplesフォルダではRuby組み込みのワンライナーを使っていて、こちらでも同じように動きます。
ruby -run -e httpd . -p 8080
どちらもWEBrickを使いますが、WEBrickはRuby 3.0から標準ライブラリに含まれなくなりました。先にgem install webrickが必要になるかもしれません。
「書きやすい構文」
もう一つの部分がjsg.rbです。いくつかのショートカットを持つJSGモジュールを定義し、JS::Objectにメソッドを追加します。リポジトリのサンプルページで実行しているRubyコードはこちらです。
require 'jsg'
$window = JSG.global
$d = $window.document
$d.getElementById("spinner").style.display = "none"
$d.createElement("h2").tap {|e|
e.innerText = "Examples"
$d.body.appendChild(e)
}
JS.global[:document]と書く代わりに$window.documentと書けて、DOMのメソッドもJavaScriptと同じ感覚で呼び出せます。ソースコードで追加されている機能は次のとおりです。
JSGのショートカット:グローバルオブジェクトを返すJSG.window(別名JSG.w)、JSG.document(別名JSG.d)、JSG.querySelectorAll(別名JSG.q)。それ以外の呼び出し、たとえばJSG.globalはJSモジュールにそのまま渡されます。- Rubyの型への変換:
to_rbメソッドが、JavaScriptの数値をFloatに、文字列をStringに、真偽値をtrue/falseに、BigIntをIntegerに、配列をRubyのArrayに変換します。それ以外のオブジェクトはJS::Objectのままです。 - プロパティとメソッド呼び出し:
method_missingがプロパティを読み、関数を呼び出し、結果をto_rbで変換します。名前が=で終わればプロパティへの代入、?で終わればRubyの真偽値を返します。 - 比較:
==でRubyのtrue、false、nilと比較できるので、JS::Trueと書く必要がありません。ほかにnil?、undefined?、typeof?(:string)もあります。 - 配列:
to_aはJavaScriptのArray.fromを使うので、NodeListのような配列風のオブジェクトもRubyのArrayになります。eachでJavaScriptの配列を順に処理できます。
テンプレートのindex.htmlには開発用の工夫もあります。require_relativeを再定義して、JS::RequireRemote経由でファイルをHTTPで読み込むようにしています。コメントによると、これはjsg buildではなくrequire_relativeを使いたい場合にだけ必要なものです。
現状
jsgは小さく、まだ荒削りなところがあります。試してみてうまく動かないところがあっても、ソースは短く、ブラウザ側のRubyファイルが一つとexe/jsgのスクリプトが一つだけです。IssueやPull RequestはGitHubでお待ちしています。ライセンスはMITです。
ruby.wasmは私の別のプロジェクトにも登場します。clogsを使ったHackety Hackの移植には、WebAssemblyにコンパイルしたCRubyで動くブラウザ版があります。