Edited at

オールRubyでフロントエンド開発を夢見て <デモあり>

More than 1 year has passed since last update.


更新サマリー

- 2018/9/7 初投稿

- 2018/9/8 iOSについて追記



はじめに

こんにちわ。関西で組み込み系のフリーランスをしている のよん(id:noontage) といいます。

昨年に こんな 記事を書かせていただきましたが、その頃からRubyでフロントエンド開発できるんじゃね?くらいには思っていました。

ちょうど、本業のプロジェクトが無事終了し時間的余裕があったのと(営業しろ)、

Web界隈は面白そうだなと思いつつも、本業では組み込みメインでWeb系の経験が少ないので、勉強がてらにプロトタイプを作ってみようと思いました。


構想

コマンドラインツール類(*-cli系)を作って、ServiceWorkermanifest.jsonを作って、PWA on Ruby行くぜ!

・・・と意気込みましたが、理想が高すぎるとモチベーションが持たないので、目標をぐっと下げます。

今回は、とりあえず JavaScript を動的に Ruby から呼べるようするのを目標とします。

もちろん環境はWebブラウザで。


成果物


デモ1


URL

は? という感じですね。本当にRubyで実行しているの?と思われたでしょう。

実際にRubyコード(正確にはmrubyのバイトコード)が実行されているのですが、これじゃあデモにならないだろうと次のデモを準備しました。


デモ2

あ、言うのを忘れておりましたが、今回のランタイムに 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.mrbfetch() して読み込みます。

次に、ランタイムである mqrb-core.wasmfetch() して読み込みます。

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) ソースコード


(おまけ2) 名前について

自動車関連企業の方とお話することがありまして、初めてモノコックという単語を知りました。

覚えたばかりの言葉は使いたくなるものです。


(おまけ3) デモのバグ

デモページ限定ですが、何度も実行するとコールバック時にコンソールにエラーが表示されることがあります。

これは、Rubyによって単にエレメントに対してJavaScriptのイベントを登録しますが、再度Rubyスクリプトを実行すると

前回のインスタンスは消えるので無効なコールバックになっているためです。

ちゃんとやるのであれば、removeeventlistener などで消すべきですが、デモなので割愛します。(逃げ)


営業しなきゃ(白目)