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

> *更新サマリー*
>- 2018/9/7 初投稿
>- 2018/9/8 iOSについて追記

## はじめに

こんにちわ。関西で組み込み系のフリーランスをしている *のよん(id:noontage)* といいます。
昨年に **[こんな](https://qiita.com/noontage/items/da846e78eebd1d3b2a78)** 記事を書かせていただきましたが、その頃からRubyでフロントエンド開発できるんじゃね？くらいには思っていました。

ちょうど、本業のプロジェクトが無事終了し時間的余裕があったのと(~~営業しろ~~)、
Web界隈は面白そうだなと思いつつも、本業では組み込みメインでWeb系の経験が少ないので、勉強がてらにプロトタイプを作ってみようと思いました。


## 構想

コマンドラインツール類(＊-cli系)を作って、`ServiceWorker`と`manifest.json`を作って、**PWA on Ruby**行くぜ！
・・・と意気込みましたが、理想が高すぎるとモチベーションが持たないので、目標をぐっと下げます。

今回は、とりあえず **JavaScript** を動的に **Ruby** から呼べるようするのを目標とします。
もちろん環境はWebブラウザで。


## 成果物

### デモ1

#### URL

- アクセスすると Rubyが実行されます。  
  https://mqrb.api.udp.xyz/demo/simple/

![image.png](https://qiita-image-store.s3.amazonaws.com/0/198132/3efc047c-d787-0e45-ac2b-8885ac959c09.png)

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



### デモ2

####

- Monocoque-Ruby <ﾓﾉｺｯｸﾙﾋﾞｰ>    
  https://mqrb.api.udp.xyz/demo/with-editor/

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



#### 使い方

<img width="837" alt="chrome-win.png" src="https://qiita-image-store.s3.amazonaws.com/0/198132/4085c571-98f6-271f-41ad-6583eb715011.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さん作](https://github.com/mattn/mruby-json)の `mruby-json` をバンドルしています。

ちなみに、

- デモ1: Rubyコンパイラなし(Rubyの文字列を直接実行できませんが軽量)、
- デモ2: Rubyコンパイラあり(Rubyの文字列を直接実行できる)

でビルドしています。

mruby自体のCコンパイラは前回同様 **Emscripten** を利用しています。
前回の記事ではソースコードから生成しましたが、今回は Homebrew でインストールした Emscripten 利用しました。

### 技術的なこと

#### デモ1の例

[デモ1](https://mqrb.api.udp.xyz/demo/simple/)の例でみてみます。
デモ1は最もシンプルな構成になっています。ブラウザでHTMLのソースコードを覗いてみてください。

重要なところだけ列挙します。

```javascript
// 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の例

![image.png](https://qiita-image-store.s3.amazonaws.com/0/198132/4192a330-4d16-a1d9-fdb4-54751740d4b4.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では読み込み後１分ほどでSafariがクラッシュしていしまいます。

iOSのデバッギングには詳しくないのですが検索したところ Macでデバッグできるようなのでデバッガーにかけてみましたが、
下記の画像の通り「クラッシュしました」のキャプションのみで詳細がわかりません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/198132/64cd10be-f350-a3f5-0461-557e16dd90c5.png)

iOSのデバッギングに詳しい方で「こんな方法で確認してみては？」というお知恵がありましたら
是非教えていただれば幸いです。

> 追記: ※safariのバグみたいです。iOS12に期待。


## 最後に(妄想)

思ったよりサクっと実装できました。Emscriptenすごい。mrubyすごい。
ソースコードへのリンクは公開します。[マサカリは投げて](https://qiita.com/kaityo256/items/d7d8f7a0e522895c8f5d)頂いて大丈夫です(笑

PWAはともかく、SPA(Single Page Application)ができるようになるとテンションがあがりますかね。
PIXI.jsをラップして、学生時代お世話になったStarRubyのような簡易的なゲームエンジンも面白いかもしれません。
（そういえば、StarRubyの公式サイトが消滅してる・・・）

その前に、iOSで動かないのは結構つらい状況だと思いますので、早急に直したいですね。
（確認したデバイス１台なので端末依存の可能性もありますが）

ところで、実装がかなり終わったところで、webrubyというものを見つけてしまいましたが、コンパイルが通りません。
４，５年ほどメンテされてないようです。(~~よかった~~)

## (おまけ1) ソースコード

- Monocoque-Ruby 
  https://github.com/noontage/monocoque-ruby


- mqrb-demo 
  https://github.com/noontage/mqrb-demo

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

自動車関連企業の方とお話することがありまして、初めてモノコックという単語を知りました。
覚えたばかりの言葉は使いたくなるものです。

## (おまけ3) デモのバグ

デモページ限定ですが、何度も実行するとコールバック時にコンソールにエラーが表示されることがあります。
これは、Rubyによって単にエレメントに対してJavaScriptのイベントを登録しますが、再度Rubyスクリプトを実行すると
前回のインスタンスは消えるので無効なコールバックになっているためです。

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

-----


営業しなきゃ（白目）