読者です 読者をやめる 読者になる 読者になる

平凡なエンジニアの独り言 はてなブログ出張所

ピアノをこよなく愛するエセRubyistが適当に書き綴ります

mrubyをNaCl(Native Client)でビルドしてみた(Windows 7)

Ruby 2.0ではNaCl対応するらしいですが、もともとNaCl自体が実行ファイルのサイズが肥大化しやすいうえに、Rubyという巨大なコードを載せてしまっては、実用上厳しいのではと感じていました。実際、20MB位になるらしいです。

そこで、組み込み用途として開発されたmrubyなら少しはましになるんじゃないかと思い、ビルドを試してみることにしました。そのまますんなりとはいかないのですが、とりあえずビルドは成功したので手順を掲載します。

前準備

mrubyと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年ぶりなので、内容的に心もとない感じです。突っ込み大歓迎です。

*1:それぞれのbinを環境変数のPATHに追加してください