更新サマリー
- 2018/9/7 初投稿
- 2018/9/8 iOSについて追記
はじめに
こんにちわ。関西で組み込み系のフリーランスをしている のよん(id:noontage) といいます。
昨年に こんな 記事を書かせていただきましたが、その頃からRubyでフロントエンド開発できるんじゃね?くらいには思っていました。
ちょうど、本業のプロジェクトが無事終了し時間的余裕があったのと(営業しろ)、
Web界隈は面白そうだなと思いつつも、本業では組み込みメインでWeb系の経験が少ないので、勉強がてらにプロトタイプを作ってみようと思いました。
構想
コマンドラインツール類(*-cli系)を作って、ServiceWorker
とmanifest.json
を作って、PWA on Ruby行くぜ!
・・・と意気込みましたが、理想が高すぎるとモチベーションが持たないので、目標をぐっと下げます。
今回は、とりあえず JavaScript を動的に Ruby から呼べるようするのを目標とします。
もちろん環境はWebブラウザで。
成果物
デモ1
URL
- アクセスすると Rubyが実行されます。
https://mqrb.api.udp.xyz/demo/simple/
は? という感じですね。本当にRubyで実行しているの?と思われたでしょう。
実際にRubyコード(正確にはmrubyのバイトコード)が実行されているのですが、これじゃあデモにならないだろうと次のデモを準備しました。
デモ2
- Monocoque-Ruby <モノコックルビー>
https://mqrb.api.udp.xyz/demo/with-editor/
あ、言うのを忘れておりましたが、今回のランタイムに Monocoque-Ruby 名前をつけています。(略称mqrb)
名前は大事です。ほら、偉人もおっしゃっているじゃないですか。
(※タイムリーです。偶然です。)
使い方
要素のレイアウトがズレていますがデモなのでご愛嬌。
左側の画像やフォームはサンプルで設置してあるものです。クリックしても何もおこらない と思います。一旦無視してください。
右側のエディタの内容が、今回実行するRubyスクリプトです。 [Execute Ruby Script] ボタンをクリックすると書かれた内容が実行されます。
サンプルでは、左側のエレメントのイベントハンドラの設定やCSSの適用などを行っています。
とりあえず、[Execute Ruby Script]をクリックしてみましょう。
すると、Rubyスクリプトが実行されて左側のフォームが動作するようになります。
なお、コードは自由に書き換えられるので、試しにお好きなRubyコードを実行してみてください。
標準出力は最終的に console.log
へ飛ばしているのでブラウザの開発ツールなどで確認してください。
説明
現在の実装では、JavaScriptObject
クラスは JavaScriptのオブジェクトを表現しています。
#new
で生成直後では JavaScriptのルートオブジェクトを参照しています。
メタプログラミングを使ったハッキーなことをしており、JavaScriptObject.new.alert 'Hello!'
のように、あたかもRubyのメソッドのように呼べます。
(興味のある方は method_missing
で検索してください。)
仕組み
開発環境
まず Ruby は Webassembly(以下:wasm) へコンパイルされた mruby で実行されています。
mrbgemsは概ね標準のものとmattnさん作の mruby-json
をバンドルしています。
ちなみに、
- デモ1: Rubyコンパイラなし(Rubyの文字列を直接実行できませんが軽量)、
- デモ2: Rubyコンパイラあり(Rubyの文字列を直接実行できる)
でビルドしています。
mruby自体のCコンパイラは前回同様 Emscripten を利用しています。
前回の記事ではソースコードから生成しましたが、今回は Homebrew でインストールした Emscripten 利用しました。
技術的なこと
デモ1の例
デモ1の例でみてみます。
デモ1は最もシンプルな構成になっています。ブラウザでHTMLのソースコードを覗いてみてください。
重要なところだけ列挙します。
// https://mqrb.api.udp.xyz/demo/simple/
fetch('demo.mrb') // コンパイル済みのrubyバイトコードを読み込む
.then(responce => responce.arrayBuffer())
.then(buffer => new Uint8Array(buffer))
.then(mrbByteCode => {
fetch('mqrb-core.wasm')
.then(response => response.arrayBuffer())
.then(buffer => new Uint8Array(buffer))
.then(binary => {
var moduleArgs = {
wasmBinary: binary,
onRuntimeInitialized: function () {
var mqrb_initialize = module.cwrap('mqrb_initialize', 'number');
var mqrb_create_instance = module.cwrap('mqrb_create_instance', 'number');
var mqrb_exec_irep = module.cwrap('mqrb_exec_irep', null, ['number', 'array']);
mqrb_initialize();
var mrb = mqrb_create_instance();
mqrb_exec_irep(mrb, mrbByteCode);
}
};
module = Module(moduleArgs);
});
});
Rubyインスタンスの生成までは、JavaScriptの仕事です。
まず最初は単に コンパイル済みRubyバイトコードである demo.mrb
を fetch()
して読み込みます。
次に、ランタイムである mqrb-core.wasm
を fetch()
して読み込みます。
EmscriptenのAPIで onRuntimeInitialized
に関数を登録するとwasmの初期化が完了したら呼ばれるようになります。
そこに、EmscriptenのAPI cwrap
で mqrbのCの関数を定義しています。
あとはインスタンス初期化(内部的にはmrb_openなど)し、先程読み込んだバイトコードを流し込んでいるだけです。
デモ2の例
まず、最初に言及しなければならないのが、DOM操作やブラウザのイベントリスナーは(現状では)直接WebAssemblyから操作できません。
従って必ずJavaScriptを経由する必要があります。
また、RubyからJavaScriptへ橋渡しする際、Rubyの値は適時JavaScriptの型に変換するように実装しています。
このとき、引数に RubyのProc
(lambda含む)クラスがあれば、内部でプロシージャを覚えておきそのインデックスのみをJavaScriptへ返しています。
イベントが発生したときは、JavaScriptからRuby側へ前述のインデックスを引数として呼び出します。(コールバック)
呼び出されたRubyは、記憶したインデックスからバイトコードを取り出し実行します。
今回は簡単な仕組のご紹介となりましたが、気が向いたら詳細な実装も投稿できればと思います。
(例えば、コールバック時のProcオブジェクトは何もしなければGCの対象になってしまいますが、その対策も行っています。)
動作環境
以下の環境で動作確認しました。
| 項目 | Windows | Mac | Android | iOS(iPadのみ)
|---|---|------| ---| ---- | --- |
| Chrome | ○ | ○ | ○ | -
| Safari | -| ○ | - | △(後述)
| Firefox | ○| ○ | 未確認 | -
| Edge| ○ | - | - | -
考察
なお、パフォーマンスのベンチマークなどはとっていません。
(標準的な指標がわかりませんでした。みなさんどうやって測っているのでしょうか。)
データサイズ
項目 | Rubyコンパイラあり | Rubyコンパイラなし |
---|---|---|
WASMサイズ | 1.2MB | 960KB |
JSサイズ | 188KB | 117KB |
合計サイズ | 約1.39MB | 約1MB |
大きいか小さいかのご判断はお任せします。
参考までに、他のWebサイトのサイズを貼っておきます。(Chrome開発ツールで測定/広告などにより変動あり/XHRは無視)
- メルカリトップページ: 1.2MB
- Yahoo! Japanトップページ: 1.6MB
- Twitterのトップページ: 4.4MB
- 価格.com: 4.7MB
- はてぶトップページ: 5.4MB
- Amazon.co.jp トップページ: 7.2MB
- 阿部寛のホームページ: 75.5KB
既存のバグ
iOSの環境は iPad Pro 9.7インチしか持っていないのですが、現状では私のiPadでは読み込み後1分ほどでSafariがクラッシュしていしまいます。
iOSのデバッギングには詳しくないのですが検索したところ Macでデバッグできるようなのでデバッガーにかけてみましたが、
下記の画像の通り「クラッシュしました」のキャプションのみで詳細がわかりません。
iOSのデバッギングに詳しい方で「こんな方法で確認してみては?」というお知恵がありましたら
是非教えていただれば幸いです。
追記: ※safariのバグみたいです。iOS12に期待。
最後に(妄想)
思ったよりサクっと実装できました。Emscriptenすごい。mrubyすごい。
ソースコードへのリンクは公開します。マサカリは投げて頂いて大丈夫です(笑
PWAはともかく、SPA(Single Page Application)ができるようになるとテンションがあがりますかね。
PIXI.jsをラップして、学生時代お世話になったStarRubyのような簡易的なゲームエンジンも面白いかもしれません。
(そういえば、StarRubyの公式サイトが消滅してる・・・)
その前に、iOSで動かないのは結構つらい状況だと思いますので、早急に直したいですね。
(確認したデバイス1台なので端末依存の可能性もありますが)
ところで、実装がかなり終わったところで、webrubyというものを見つけてしまいましたが、コンパイルが通りません。
4,5年ほどメンテされてないようです。(よかった)
(おまけ1) ソースコード
-
Monocoque-Ruby
https://github.com/noontage/monocoque-ruby -
mqrb-demo
https://github.com/noontage/mqrb-demo
(おまけ2) 名前について
自動車関連企業の方とお話することがありまして、初めてモノコックという単語を知りました。
覚えたばかりの言葉は使いたくなるものです。
(おまけ3) デモのバグ
デモページ限定ですが、何度も実行するとコールバック時にコンソールにエラーが表示されることがあります。
これは、Rubyによって単にエレメントに対してJavaScriptのイベントを登録しますが、再度Rubyスクリプトを実行すると
前回のインスタンスは消えるので無効なコールバックになっているためです。
ちゃんとやるのであれば、removeeventlistener
などで消すべきですが、デモなので割愛します。(逃げ)
営業しなきゃ(白目)