mrubyをブラウザで実行するまで (WebAssembly)

> *更新サマリー*
>- 2017/11/28 print問題について加筆した。/独自マクロを除去。標準のものへ変更した。その他細かい文を修正した。
>- 2018/7/19 幾つか正しくない表記を変更した。

## はじめに

### Rubyがブラウザで動作する

先日、主要ブラウザで**WebAssembly**を利用できる環境が整ったと話題になりました。

- [(参考リンク) WebAssembly対応、主要ブラウザChrome/Firefox/Edge/Safariで整う。](http://www.publickey1.jp/blog/17/webassembly_browsers.html)

このことから、今後はWebアプリ（特にフロントエンド）でJavaScript系列以外の言語が選択肢にはいるようになります。(検索すると、Rustがよく引き合いに出されています。)

本コンテンツは**mrubyインタプリタ**を**WebAssembly**に変換し、ブラウザ上でRubyコード`"p 'hello world! ...'`を動作させるところまでを目標とします。なお、余談ですが、理屈上CRubyもWebAssembly化できるはずですが、ここでは**mruby**をつかいます。


### 対象者

「mrubyとは」や「WebAssemblyとは」といった説明はありませんので基本的な概念は知っておく必要があります。

ただし、「よくわからんけど、**mrubyはRubyの軽量版**で、**WebAssemblyはブラウザでC言語がうごくやつやろ？」**くらいのノリでも読めるようには工夫します。


## 環境準備

本コンテンツ執筆にあたり試した環境は以下になります。

| 項目 | 値 | 備考 |
|---|---|------|
| OS | macOS Sierra |
| ブラウザ | Google Chrome 62.0.3202.94 |
| mruby | 31ce73dd (2017/11/18時点のmaster)  | 導入方法は後述
| コンパイラ(ネイティブ) | clang-900.0.38 Apple LLVM version 9.0.0 | デフォルトのもの
| コンパイラ(クロス) | Emscripten Compiler Frontend　1.37.22 | 導入方法は後述
| その他| git,cmake | brew等でインストールしてください

その他、最後の動作確認で webサーバが必要ですが、本ドキュメントでは Dockerで紹介しています。
（WebサーバであればなんでもOK。 nodejs　の http-server や apache など。）


## Emscripten Compiler Frontend(ビルドツール)のセットアップ

### Emscripten Compiler Frontendの構築方法

Emscripten Compiler Frontend（以降:emcc)とは、C言語のソースをWASM（WebAssemblyのバイナリ）に変換するためのツールです。
厳密には途中でLLVMのビットコードに変換されてたりしますが使用上では気にしなくて良さそうです。

以下のドキュメント「Emscripten の環境設定」までを一通り実行します。

[C/C++からWebAssemblyにコンパイルする]
https://developer.mozilla.org/ja/docs/WebAssembly/C_to_wasm

ターミナルから`emcc -v`など`emcc`コマンドが使える状態になればOKです。
参考までに私が試したコマンドを記述します。（コマンドの詳細は上記を参照ください）

```zsh
git clone https://github.com/juj/emsdk.git
cd emsdk

# 20-30分くらい時間がかかります。溜まったアニメを消化しましょう。
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit

# emccのパスが通るので展開場所を意識したほうがいいかもしれません。
./emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit

# 下記を実行することで`emcc`へのパスが張られます。
source ./emsdk_env.sh
```


## mrubyのセットアップ

### mrubyのダウンロード

mruby公式リポジトリから`clone`します。`clone`できたらディレクトリに入ります。

```zsh
git clone https://github.com/mruby/mruby.git
cd mruby
```
>実はこの時点で `make` を叩くとビルドされ、実行されているホスト向けのmrubyバイナリ(->mrbc等)とライブラリ(->libmruby.a)ができます。さすがmruby、お手軽ですね。


### WebAssembly向けにビルド設定の変更

#### クロスコンパイルとは

今回は、PC向けではなく**WebAssembly**のバイナリを吐いてほしいのでビルドの設定を変更します。
ホストとは異なるアーキテクチャへのコンパイルを行うことを**クロスコンパイル**と言います。
mrubyは組み込みでの利用が想定されているため、クロスコンパイルのコンフィグレーションはとても洗練されています。

#### クロスコンパイルの設定

ここでは概ね、[公式ドキュメント](https://github.com/mruby/mruby/blob/master/doc/guides/compile.md)のクロスコンパイルの方法に沿って設定します。
まず、直下の `build_config.rb` を開き、一番最後に下記を追加します。

```Ruby
# build_config.rb
...
# Define wasm build settings
MRuby::CrossBuild.new('wasm') do |conf|
  toolchain :clang

  # C compiler settings
  conf.cc do |cc|
    cc.command = 'emcc' # 通常のコンパイルは emccコマンド（フロントエンド） を使います。
    cc.flags = [ENV['CFLAGS'] || %w()]
    cc.include_paths = ["#{root}/include"]
    cc.option_include_path = '-I%s'
    cc.option_define = '-D%s'
    cc.compile_options = "%{flags} -c %{infile} -s WASM=1 -o %{outfile}"
  end

  # Archiver settings
  # ※本来はオブジェクトファイルを固めるものですが、ここではemccでbitcodeを固めます。
  conf.archiver do |archiver| 
    archiver.command = 'emcc'
    archiver.archive_options = '%{objs} -s WASM=1 -o %{outfile}'
  end

  # file extensions
  # 成果物、中間ファイルの拡張子を設定します。
  conf.exts do |exts|
    exts.object = '.bc'
    exts.executable = '' # '.exe' if Windows
    exts.library = '.bc'
  end

  conf.gembox 'wasm'  # 後述
end
```

### gemboxのセットアップ

#### gemboxとは

`gembox`とは使用する`mrbgem`のセットです。つまり、`conf.gembox 'wasm'`はwasm.gembox を使うという意味で、gembox自体は `./mrbgems/`内に設置します。

> **mrbgemってなに**
> `mrbgem`とはCRubyでいう`gem`のmrubyバージョンです。CRubyと異なり**ビルド時にすべて包括**します。 
> `gembox`は使う`mrbgem`を列挙したものです。

実はデフォルトで`default.gembox`が存在しそれを使うことができるのですが、不必要なものがあるためいくつか取り除きます。また、どうやらemccでは　`timespec_get(...)`の実装がないらしく、今回は残念ながら`Time`クラスは妥協します。


#### gemboxの作成(for wasm)

`default.mrbgem`をコピーして`wasm.gembox`を作成します。

```zsh
cp mrbgems/default.gembox mrbgems/wasm.gembox
```

下記のようにコメントアウトします。

```Ruby

 11   # Use standard Time class
 12   # conf.gem :core => "mruby-time"  <- コメントアウト (実装上の理由)
...
 65   # Generate mirb command
 66   #conf.gem :core => "mruby-bin-mirb"  <- コメントアウト
 67
 68   # Generate mruby command
 69   #conf.gem :core => "mruby-bin-mruby"  <- コメントアウト
 70
 71   # Generate mruby-strip command
 72   #conf.gem :core => "mruby-bin-strip"  <- コメントアウト
```


### 標準出力の修正

>- 2017/11/28追記
>  - `fflush()`関数の実装の仕様で`\n`がない`fflush()`関数は動作しないため対処を追記
>  - https://github.com/kripken/emscripten/issues/2770

実はもう１つ修正すべきところがあります。
mrubyの`puts`や`p`メソッドはC言語の`printstr()`関数が呼び出されています。
35行目付近が実際に出力しているところで、よく知られている、`printf()`関数ではなく`fwrite()`で標準出力しています。
(`printstr()`の実装は`./mrbgems/mruby-print/src/print.c`にあります。)

```C
# mrbgems/mruby-print/src/print.c
...
 35    fwrite(RSTRING_PTR(obj), RSTRING_LEN(obj), 1, stdout);
 36    fflush(stdout);
```

標準出力されたストリームの旅を３行で書くと下記のようになります。

1. 標準出力されたストリームはごにょごにょされて、C言語の世界では最後にシステムコールの`SYS_writev`が呼ばれJavaSciptの世界へいきます。
1. またJavaScriptの世界でこにょごにょされ最終的に`console.log`が呼ばれます。[C言語の標準出力の果てはここ](https://github.com/kripken/emscripten/blob/7f89560101843198787530731f40a65288f6f15f/src/shell_minimal.html#L89)
3. ごにょごにょの部分はバッファリングや改行がある場合はバッファからフラッシュするなど地味だけど大事なことをやっています。

３行目にさらっと書いていますが、**改行がある場合はフラッシュ**ということは、**改行があるまで出力されない** ということです。
この仕様のため、標準のmrubyの実装ではうまく動作しません。そのため、今回は以下のように修正して動作するようにします。

C言語側の修正

- 文字列の最後が改行の場合
 - 修正なし
- 文字列の最後が改行でない場合
  - Emscriptenのシェルは処理中の文字コード`0`の場合もフラッシュする挙動になっているため、`0`を書き込むことで強制的にフラッシュさせる。([ここ](https://github.com/kripken/emscripten/blob/7f89560101843198787530731f40a65288f6f15f/src/library_syscall.js#L797))

Ruby側の修正

- Rubyの`puts`や`p`は文字列と改行を分けて出力しているがまとめる。

具体的には、以下のようにします。

```C
// ./src/print.c
...
static void
printstr(mrb_state *mrb, mrb_value obj)
{
  if (mrb_string_p(obj)) {
#if defined(_WIN32)
    if (isatty(fileno(stdout))) {
      DWORD written;
      int mlen = (int)RSTRING_LEN(obj);
      char* utf8 = RSTRING_PTR(obj);
      int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, mlen, NULL, 0);
      wchar_t* utf16 = (wchar_t*)mrb_malloc(mrb, (wlen+1) * sizeof(wchar_t));
      if (utf16 == NULL) return;
      if (MultiByteToWideChar(CP_UTF8, 0, utf8, mlen, utf16, wlen) > 0) {
        utf16[wlen] = 0;
        WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),
          utf16, wlen, &written, NULL);
      }
      mrb_free(mrb, utf16);
    } else
#endif
      fwrite(RSTRING_PTR(obj), RSTRING_LEN(obj), 1, stdout);
#ifndef EMSCRIPTEN
    fflush(stdout);
#else // WASMビルド時
    if(RSTRING_LEN(obj)>0){
      if(RSTRING_PTR(obj)[RSTRING_LEN(obj)-1] != 10){ // LF(改行)じゃなかったら
        fwrite(NULL, 1, 1, stdout);        // 強制flush
      }
    }
#endif
  }
}
...
```

Ruby側も修正します。

```ruby
# ./mrblib/print.rb
...
  def puts(*args)
    i = 0
    len = args.size
    while i < len
      s = args[i].to_s
      s += "\n" if (s[-1] != "\n") # 一気に出力させる
      __printstr__ s
      i += 1
    end
    __printstr__ "\n" if len == 0
    nil
  end
...
  def p(*args)
    i = 0
    len = args.size
    while i < len
      __printstr__ args[i].inspect + "\n" # 一気に出力させる
      i += 1
    end
    args[0]
  end
```

## mrubyのビルド

もう少しです。がんばりましょう。
ここまでできたら、`make`を実行しビルドします。

```zsh
make
```

ちょっとだけ時間がかかりますが、emccほどではありません。
以下のようなメッセージがでればビルド成功です。

```zsh
================================================
      Config Name: wasm
 Output Directory: build/wasm
    Included Gems:
             mruby-sprintf - standard Kernel#sprintf method
             ... #中略
             mruby-class-ext - class/module extension
             mruby-compiler - mruby compiler library
================================================
```

これで、WebAssembly向けのmrubyライブラリができました。


## Hello Worldを作る

ここでは、作成したmrubyライブラリを使った　HelloWorldを作成します。
ビルドしたmrubyから、１つ上のディレクトリに移動し`hello`ディレクトリを作ります。
ディレクトリを作成したら移動します。

```
cd ..
mkdir hello
cd hello
```

ほぼ[[公式ドキュメント](https://github.com/mruby/mruby/wiki/Hello-World)]どおりですが、直下に`hello.c`を作成し、以下のようにコーディングします。

```C
#include <stdio.h>
#include <mruby.h>
#include <mruby/compile.h>

int
main(void)
{
  mrb_state *mrb = mrb_open();
  if (!mrb) { /* handle error */ }
  puts("Executing Ruby code from C!");
  mrb_load_string(mrb, "p 'hello world! This message is executed Ruby!'"); // <- ここのRubyコードが実行される!
  mrb_close(mrb);
  return 0;
}
```

コンパイルします。オプションについては[[公式ドキュメント](https://kripken.github.io/emscripten-site/docs/tools_reference/emcc.html#arguments)]を参照してください。

```zsh
emcc hello.c -I ../mruby/include ../mruby/build/wasm/lib/libmruby.bc -O2 -s WASM=1 -o hello.html
```

> なお、`-O2`(最適化)をつけていますが、外した場合はサイズが大きいため以下のようなメッセージがでます。
> `warning: emitted code will contain very large numbers of local variables, which is bad for performance (build to JS with -O2 or above to avoid this - make sure to do so both on source files, and during 'linking')`
> (ファイルサイズも 400KBほど大きくなります)

ここまでの時点で以下のファイルができているはずです。

```zsh
ls
# hello.c    hello.html hello.js   hello.wasm
```

## ブラウザで試してみる

### Webサーバへアップロードする

WebAssemblyのAPI自体が他からリソースをとる(XHR,Fetch)形式のためローカルで実行できません。
お好みのWebサーバに 、`hello.html / hello.js / hello.wasm` をアップロードしましょう。

ここでは、DockerでAapcheを立てますがお好きなWebサーバでお試しください。

下記コマンドでカレントディレクトリ内がドキュメントルートになったWebサーバができあがります。

```zsh
docker run -dit --name my-apache-app -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:alpine
```


## 動作確認

ブラウザでレッツアクセス!

- http://localhost:8080/hello.html

![image.png](https://qiita-image-store.s3.amazonaws.com/0/198132/f157ab13-eec8-2e08-cc8c-90cd5d05a21b.png)


### デベロッパーツールも確認してみる

![image.png](https://qiita-image-store.s3.amazonaws.com/0/198132/28e7dce4-00d3-bb8e-d1d3-90c36c84cfa6.png)

ちゃんと出力されてます！

> ※追記
> 画像のエラーはApacheの MIMEがちゃんと設定されてないから表示されるようです。
> AddTypeなどで application/wasm を追加すれば消えます。


## 総括

ブラウザで、Opal等のエミュレータではなく、ネイティブのRubyが動かせる時代がきそうです!
(個人的にはRPGツクールのRGSSとか移植できたら面白そうとか思ったりした)


## おまけ

サンプルで利用したものを公開しておきます。

- mruby
  - https://github.com/noontage/test-mruby-wasm
- mruby-app
  - https://github.com/noontage/test-mruby-wasm-app1