Help us understand the problem. What is going on with this article?

オール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

image.png

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

デモ2

あ、言うのを忘れておりましたが、今回のランタイムに Monocoque-Ruby 名前をつけています。(略称mqrb)
名前は大事です。ほら、偉人もおっしゃっているじゃないですか。
(※タイムリーです。偶然です。)

使い方

chrome-win.png

要素のレイアウトがズレていますがデモなのでご愛嬌。

左側の画像やフォームはサンプルで設置してあるものです。クリックしても何もおこらない と思います。一旦無視してください。

右側のエディタの内容が、今回実行する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の例

image.png

まず、最初に言及しなければならないのが、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でデバッグできるようなのでデバッガーにかけてみましたが、
下記の画像の通り「クラッシュしました」のキャプションのみで詳細がわかりません。

image.png

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 などで消すべきですが、デモなので割愛します。(逃げ)


営業しなきゃ(白目)

noontage
いろいろバックエンドな人。組み込み専攻なのにNWエンジニアとして就職し、独学でRubyを学んだかと思えばなぜかJavaをやっていました。現在はフリーで組み込み系やったかと思えばWeb系2年位やってる謎の経歴。
https://tcp.udp.xyz/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away