mrubyをNaCl(Native Client)でビルドしてみた(Windows 7)
Ruby 2.0ではNaCl対応するらしいですが、もともとNaCl自体が実行ファイルのサイズが肥大化しやすいうえに、Rubyという巨大なコードを載せてしまっては、実用上厳しいのではと感じていました。実際、20MB位になるらしいです。
そこで、組み込み用途として開発されたmrubyなら少しはましになるんじゃないかと思い、ビルドを試してみることにしました。そのまますんなりとはいかないのですが、とりあえずビルドは成功したので手順を掲載します。
前準備
mrubyとNaClのビルド環境を準備してください。
- Windows 7
- mrubyのビルド環境(MinGW, msys)*1
- NaClのビルド環境を作る
- ちょっと面倒です。こちらを参照してください
手順の概要
mrubyは、ビルド環境さえ整えればmakeをたたくだけでビルドできるという親切設計です。NaClでビルドする際、障害になるのはCコードを自動生成している部分です。ビルド中にrubyのコードをバイトコード化してcに埋め込む作業を行っているらしく、その際にmrbc.exeというバイトコードを生成する実行ファイルを生成・実行しているのですが、NaClではnexeを生成するので、mrbc.exeの実行に失敗してしまいます。
そこで、mrubyのMinGWでのビルドを行ってCコード(mrblib.c)を生成してから、オブジェクトファイル(*.o)を削除して、NaClでビルドするというトリッキーな手順を踏みました。
libmruby.aというライブラリファイルが生成されるので、これを自分のNaClのC++コードでリンクすればOKです。(mrubyはCなので、ヘッダを読み込むときにはextern "C"するのを忘れないようにしましょう。最新のコードを見たら、ヘッダの中に書かれていたので必要ないことを確認しました。)
具体的な手順
1. mrubyをMinGWでmakeしてください
2. 各フォルダのオブジェクトファイル(*.o)とlib/libmruby.aを削除してください
3. mrubyのMakefileを書き換えてください
mrubyのMakefileを、NaClのものを指定してください。以下はx86-64ですが、x86用にコンパイルするならi686-nacl-gcc.exeを指定してください。NACL_ROOTはNaCl SDKを置いたパスと置き換えてください。(私の環境だと/C/tools/nacl_sdk/)
export CC = /NACL_ROOT/pepper_19/toolchain/win_x86_newlib/bin/x86_64-nacl-gcc export LL = /NACL_ROOT/pepper_19/toolchain/win_x86_newlib/bin/x86_64-nacl-gcc export AR = /NACL_ROOT/pepper_19/toolchain/win_x86_newlib/bin/x86_64-nacl-ar.exe
4. srcに移動してmakeしてください
5. mrblibに移動してmakeしてください
6. lib/libmruby.aを使ってNaCl用に書いたC++をコンパイルしましょう
などと書いても不親切すぎるので、NaClのチュートリアルのコードを改変して試してみました。NaClもmrubyも全然わかっていませんが・・・。
#include <cstdio> #include <string> #include "ppapi/cpp/instance.h" #include "ppapi/cpp/module.h" #include "ppapi/cpp/var.h" #include "mruby.h" #include "mruby/proc.h" #include "mruby/array.h" #include "mruby/string.h" #include "mruby/dump.h" #include "mruby/irep.h" #include "mruby/compile.h" class hello_tutorialInstance : public pp::Instance { public: explicit hello_tutorialInstance(PP_Instance instance) : pp::Instance(instance) { } virtual ~hello_tutorialInstance() {} virtual void HandleMessage(const pp::Var& var_message) { if (!var_message.is_string()) return; std::string message = var_message.AsString(); pp::Var var_reply; int n; mrb_state* mrb; struct mrb_parser_state* st; mrb = mrb_open(); st = mrb_parse_string(mrb, (char*)message.c_str()); n = mrb_generate_code(mrb, st->tree); mrb_pool_close(st->pool); mrb_value result = mrb_run(mrb, mrb_proc_new(mrb, mrb->irep[n]), mrb_nil_value()); char s[256]; sprintf(s, "%d", result.value.i); var_reply = pp::Var(s); PostMessage(var_reply); } }; class hello_tutorialModule : public pp::Module { public: hello_tutorialModule() : pp::Module() {} virtual ~hello_tutorialModule() {} virtual pp::Instance* CreateInstance(PP_Instance instance) { return new hello_tutorialInstance(instance); } }; namespace pp { Module* CreateModule() { return new hello_tutorialModule(); } } // namespace pp
HTMLは以下の通りです。'125+245'という単純な足し算をmrubyに渡しています。
<!DOCTYPE html> <html> <head> <title>hello_tutorial</title> <script type="text/javascript"> hello_tutorialModule = null; // Global application object. statusText = 'NO-STATUS'; function moduleDidLoad() { hello_tutorialModule = document.getElementById('hello_tutorial'); updateStatus('SUCCESS'); //ここでRubyのコードとして'125+245'を送信 hello_tutorialModule.postMessage('125 + 245'); } function handleMessage(message_event) { alert(message_event.data); } function pageDidLoad() { if (hello_tutorialModule == null) { updateStatus('LOADING...'); } else { // It's possible that the Native Client module onload event fired // before the page's onload event. In this case, the status message // will reflect 'SUCCESS', but won't be displayed. This call will // display the current message. updateStatus(); } } function updateStatus(opt_message) { if (opt_message) statusText = opt_message; var statusField = document.getElementById('status_field'); if (statusField) { statusField.innerHTML = statusText; } } </script> </head> <body onload="pageDidLoad()"> <h1>Native Client Module hello_tutorial</h1> <p> <div id="listener"> <script type="text/javascript"> var listener = document.getElementById('listener'); listener.addEventListener('load', moduleDidLoad, true); listener.addEventListener('message', handleMessage, true); </script> <embed name="nacl_module" id="hello_tutorial" width=0 height=0 src="hello_tutorial.nmf" type="application/x-nacl" /> </div> </p> <h2>Status</h2> <div id="status_field">NO-STATUS</div> </body> </html>
感想
x86-64のファイルサイズは4MBでした。Windowsのexeは1MBちょいだった気がするので、もう少し小さくなってほしいところですが、本家のrubyよりはましになるんじゃないかなと思います。昔からNaClの実行ファイルは大きすぎる気がしているので、この辺はGoogleに頑張ってほしいところです。
NaClに詳しくないのですが、NaClが生成したmrbc.exeを実行することはできる(nexeを実行する方法があったはず)と思うので、普通に環境に応じて場合分けしたmakeファイルを書けば、こんなトリッキーなことをしなくてもビルドできるようになると思います。
mrubyは開発が活発みたいなので(この記事を書いている間も、最新版のMakefileの構造が変わったことに気づいて書き直したりしています)、フィードバックをいろいろとかえせたら有用なのかなと考えています。
# 言語をアプリに組み込むことにワクワクするのは私だけではない・・・はず。
それにしても、まともにMakefileを見るのもC++を書くのも7 ~ 8年ぶりなので、内容的に心もとない感じです。突っ込み大歓迎です。