はじめに
Rubyがブラウザで動作する
先日、主要ブラウザでWebAssemblyを利用できる環境が整ったと話題になりました。
このことから、今後はWebアプリ(特にフロントエンド)でJavaScript系以外にも他の言語の採用が選択肢にはいるようになります。(検索すると、Rustがよく引き合いに出されています。)
本コンテンツはmrubyインタプリタをWebAssemblyに変換し、「ブラウザ上でRuby(p "Hello world"
)が動作する(コンソール出力)」ところまでを目標とします。余談ですが、理屈上CRubyもWebAssembly化できるはずですが、ここではmrubyをつかいます。
対象者
「mrubyとは」や「WebAssemblyとは」といった説明はありませんので基本的な概念は知っておく必要があります。
・・・が
「よくわからんけど、mrubyはRubyの軽量版で、WebAssemblyはブラウザでC言語がうごくやつやろ?」くらいのノリでも読めるようには工夫します。
免罪符的な(読み飛ばしてOK)
中の人(noontage)の本業はWeb系ではありません。(エクセル大好きなおじさんが多いあの業界の人です。)
よって、誤った表記があるかもしれませんが、ツッコミは大歓迎ですのでフィードバックいただけますと幸いです。
環境準備
本コンテンツ執筆にあたり試した環境は以下になります。
項目 | 値 | 備考 |
---|---|---|
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サーバなら npm でも 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です。
参考までに私が試したコマンドを記述します。(コマンドの詳細は上記を参照ください)
git clone https://github.com/juj/emsdk.git
cd emsdk
# 20-30分くらい時間がかかります。アニメ1話でも消化しましょう。
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
# emccのパスが通るので展開場所を意識したほうがいいかもしれません。
./emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
source ./emsdk_env.sh
mrubyのセットアップ
mrubyのダウンロード
mruby公式リポジトリからclone
します。clone
できたらディレクトリに入ります。
git clone https://github.com/mruby/mruby.git
cd mruby
余談ですが、実はこの時点で
make
を叩くとビルドされ、実行されているホスト向けのmrubyバイナリ(->mrbc等)とライブラリ(->libmruby.a)ができます。さすがmruby、お手軽ですね。
WebAssembly向けにビルド設定の変更
クロスコンパイルとは
今回は、PC向けではなくWebAssemblyのバイナリを吐いてほしいのでビルドの設定を変更します。
ホストとは異なるアーキテクチャへのコンパイルを行うことをクロスコンパイルと言います。
mrubyは組み込みでの利用が想定されているため、クロスコンパイルのコンフィグレーションはとても洗練されています。
クロスコンパイルの設定
ここでは概ね、公式ドキュメントのクロスコンパイルの方法に沿って設定します。
まず、直下の build_config.rb
を開き、一番最後に下記を追加します。
# 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.defines = %w(_WASM) # 後述
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でllvmを固めます。
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
を作成します。
cp mrbgems/default.gembox mrbgems/wasm.gembox
下記のようにコメントアウトします。
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" <- コメントアウト
標準出力の修正
実はもう1つ修正すべきところがあります。
mrubyのputs
やp
の実装はC言語の__printstr__(...)
関数が呼び出されています。
__printstr__(...)
の実装は./mrbgems/mruby-print/src/print.c
にあります。
35行目付近が実際に出力しているところです。printf(...)
ではなくfwrite(...)
で標準出力に吐いています。
# mrbgems/mruby-print/src/print.c
...
35 fwrite(RSTRING_PTR(obj), RSTRING_LEN(obj), 1, stdout);
36 fflush(stdout);
このままではうまく動作しませんでした。(空白が出力されます。)
少し調べましたがなぜうまく動作しないのか、私もよくわかりませんでした。(詳しい方がいらっしゃったらぜひ教えてください)[ヒントになりそうなところ]
- FileSystemAPI#setting-up-standard-i-o-devices (URL)
- emccにおけるfwriteの実装 (URL)
今回はとりあえずprintf(...)
に書き換えます。ここで、先程定義した_WASM
のマクロを判定しています。
...
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
#if defined(_WASM)
if((*(RSTRING_PTR(obj)+RSTRING_LEN(obj)) == '\0')){
printf("%s",RSTRING_PTR(obj)); // WASMビルド時は printfを使う
}
#else
fwrite(RSTRING_PTR(obj), RSTRING_LEN(obj), 1, stdout);
fflush(stdout);
#endif
}
}
...
mrubyのビルド
もう少しです。がんばりましょう。
ここまでできたら、make
を実行しビルドします。
make
ちょっとだけ時間がかかりますが、emccほどではありません。
以下のようなメッセージがでればビルド成功です。
================================================
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から、1つ上のディレクトリに移動しhello
ディレクトリを作ります。
ディレクトリを作成したら移動します。
cd ..
mkdir hello
cd hello
ほぼ[公式ドキュメント]どおりですが、直下にhello.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;
}
コンパイルします。オプションについては[公式ドキュメント]を参照してください。
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ほど大きくなります)
ここまでの時点で以下のファイルができているはずです。
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サーバができあがります。
docker run -dit --name my-apache-app -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:alpine
ブラウザで動作確認
レッツアクセス http://localhost:8080/hello.html
できあがり
デベロッパーツールも確認してみる
ちゃんと出力されてます!
(・・・なんか、エラーでてるけど、これ公式のチュートリアルでもでるんだよね)
総括
ブラウザで、Opal等のエミュレータではなくネイティブのRubyが動かせる時代がきそうです!
(個人的にはRPGツクールのRGSSとか移植できたら面白そうとか思ったりした)