mrubyをとりあえず動かしてみただけ

f:id:dojineko:20160211204157j:plain

ちょっと長いよ。


Golangツールを作っていたさなか、とある日の会話。

ぼく 「クロスコンパイルできて、単一バイナリで動くツール作るにはGoいいですよね」
A氏 「それmrubyでやればいいじゃん」
ぼく 「そんな気軽に出来ないんじゃないですか?」
A氏 「mruby触ったこと無いの?できるけど(プークスクス」(意訳)
ぼく「(#^ω^)・・・。」

・・・というわけで、しぶしぶmrubyに触れることにしました。

(※ 意訳のくだりは、あくまでキャッチーなネタとして解釈してネ。)

mruby とはなんぞ?

参考までにmruby第一人者 matsumoto-r 氏のブログより一部抜粋します。

http://blog.matsumoto-r.jp/?p=3310

mrubyとは、組み込み機器やアプリ組み込みに最適化された軽量スクリプト言語です。記述方法は既存のRubyのように書くことができます。

ということで、mrubyそれ自体は基本的に「何か組み込んで使う」ことを想定されてつくられたみたい。
それ以上の「IoTでのmrubyの活用事例」とかそういうのは個別にググってみると面白い記事が見つかるかも?

当時の要件

当時ツールの実装のために重視した点は下記。

  • コード自体を楽しく書けること。
  • ロスコンパイルできること。
  • 成果物の実行にストレスが無いこと。

ということで、当時は Go がその全てを満たすので手段として採用しました。
特段 Go を贔屓にしているわけではなく、目的にあった手段を採用したというわけ。
(特にメンテナンス性とか他に書ける人がいるかとかの引き継ぎの可用性は考えずに作ってるのでそこに突っ込まないでね)

たとえば、Webサイトを作るのに、どうしてもやりたくてやる場合を除いては、
わざわざ Revel(Go) を採用してVPSやらHerokuにデプロイしたりは今のとこしないでしょう。
たぶん、RailsSinatra(Ruby)やらLaravel、Lumen(PHP)を採用します。たぶん、そういうもんです。

Hello, mruby world!

ということで、本題のmrubyに触れてみましょう。
mrubyは「何かに組み込んで使う」事もできますが、
Ruby(ここではCRubyCで実装されたRuby)と同じように使うこともできます。

まずは、mruby自体のコンパイルをしてみました。

git clone https://github.com/mruby/mruby
cd mruby
ruby ./minirake
# ずらずらとコンパイルが始まる

これで、bin配下に下記のようなバイナリが作られます。

ここまでくればすでにmrubyは手の中。ほぼRubyと同じく利用することができます。
ほぼ、としたのはRubyと完全互換なわけではないため。

mrubyは必要な機能だけ有効、あるいは新たに追加してコンパイルすることができるので、
コンパイルオプションによっては使えない機能やクラスが出てくるし、逆もしかり。
RubyでいうとGemをインストールして、requireしてやるとかいう感じ。
mrubyでいえば mrbgems というものになるそうな。mrubyでrequireはないとのことらしい。

# mirb を使ってみる
$ ./mirb
mirb - Embeddable Interactive Ruby Shell

> p "Hello, world!"
"Hello, world!"
 => "Hello, world!"
> exit
# mruby にコードを渡してみる
$ echo 'p "Hello, world!"' > hello.rb
$ ./mruby hello.rb
"Hello, world!"

ひとまずここまで、は普通といえば普通ね。 続いて、バイトコードコンパイルしてみる。

# mrbc でコードをバイトコードにコンパイルしてみる
$ ./mrbc hello.rb
$ ls -la hello.*
-rw-r--r--  1 xxxxx  xxxxx  100  2  4 22:42 hello.mrb # => mrbcでコンパイルされたソース
-rw-r--r--  1 xxxxx  xxxxx   18  2  4 22:41 hello.rb  # => 元になったソース

コンパイルしたコードは mruby に渡すことができます。
その時、実行可能なmrubyバイナリであることをオプションで通知してあげる必要があります。

# コンパイルしたソースはmrubyに渡すことができる。(-bオプション必須)
$ ./mruby -b hello.mrb
"Hello, world!"

# -b を省略したりすると syntax error そりゃそうだ。
$ ./mruby hello.mrb
hello.mrb:1:100: unterminated string meets end of file
hello.mrb:1:100: syntax error, unexpected $end

Hello, eMbeddable-RUBY world!

バイトコードまでコンパイルして見て、ふと疑問に思うはず。

「なんのためにこんなことをするのか?」

それは mrubyが 組み込み用途向け軽量Ruby だからというとこにつきるようで、
例えば基盤にmrubyを組み込むような用途では、メモリやストレージが非常にシビアなため、
書いたままのRubyコードをそのまま組み込むのが無駄が多く用途に向きません。
そこで、mrbcでバイトコードに変換して、そういった環境でも効率よくコードを利用できるようにしているというわけの様子。

ぱっと見だと、Hello,world を出力する程度であれば、コンパイルしたことで無駄に容量が増えていますが、
コード数がそこそこ膨大になってくると、効果を発揮してくれるということなのでしょう。

ちなみにバイトコード自体はmrubyが動作する環境であれば基本的にどの環境でも動くらしいです。

・・・と、いうことで、試しにHello worldを「何か」に組み込んでみることにします。
簡単にC++のコードを用意して、mrubyをプログラムから利用するスクリプトとして実行してみます。

ひとまずは、コンパイルしていないソースコードを実行してみました。

// hello_from_rb.cpp
#include <iostream>
#include <memory>
#include <mruby.h>
#include <mruby/compile.h>

using namespace std;

int main( int argc, char** argv ) {
  // ファイルを開く
  const char* fn = "hello.rb";
  shared_ptr< FILE > file(fopen(fn, "r"), []( FILE* f ){ if ( f != nullptr ){ fclose(f); } } );

  // エラー処理は程々に
  if ( file == nullptr ) {
    cout << "Load " << fn << " failed." << endl;
    return 1;
  }

  // 仮想マシンを初期化して
  shared_ptr< mrb_state > mrb(mrb_open(), []( mrb_state* p ){ if ( p != nullptr ){ mrb_close(p); } } );
  // 実行する ("Hello, World!" が出る)
  mrb_load_file(mrb.get(), file.get());

  return 0;
}

実行すると "Hello, world!" の出力が得られました。

# コンパイルして
clang++ -Wall -I../include -L../build/host/lib -o hello_from_rb hello_from_rb.cpp -lmruby -std=c++11

# 実行
./hello_from_rb
"Hello, world!"

また mrbc でバイトコードにしたものを読み込むときは下記のようになります。

// hello_from_mrb.cpp
#include <iostream>
#include <memory>
#include <mruby.h>
#include <mruby/dump.h>

using namespace std;

int main( int argc, char** argv ) {
  // ファイルを開く
  const char* fn = "hello.mrb";
  shared_ptr< FILE > file(fopen(fn, "r"), []( FILE* f ){ if ( f != nullptr ){ fclose(f); } } );

  // エラー処理は程々に
  if ( file == nullptr ) {
    cout << "Load " << fn << " failed." << endl;
    return 1;
  }

  // 仮想マシンを初期化して
  shared_ptr< mrb_state > mrb(mrb_open(), []( mrb_state* p ){ if ( p != nullptr ){ mrb_close(p); } } );
  // 実行する ("Hello, World!" が出る)
  mrb_load_irep_file(mrb.get(), file.get());

  return 0;
}
# コンパイルして
clang++ -Wall -I../include -L../build/host/lib -o hello_from_mrb hello_from_mrb.cpp -lmruby -std=c++11

# 実行
./hello_from_mrb
"Hello, world!"

実行すると同じく "Hello, world!" の出力が得られました。
ちなみにmrbcはバイトコードをCの配列形式でも出力してくれるようです。

# hello.rb 配下に hello.c として、バイトコードをCの配列形式で吐いてくれる
./mrbc -B hello hello.rb
/* dumped in little endian order.
   use `mrbc -E` option for big endian CPU. */
#include <stdint.h>
const uint8_t
#if defined __GNUC__
__attribute__((aligned(4)))
#elif defined _MSC_VER
__declspec(align(4))
#endif
hello[] = {
0x45,0x54,0x49,0x52,0x30,0x30,0x30,0x33,0xaf,0x2f,0x00,0x00,0x00,0x64,0x4d,0x41,
0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0x46,0x30,0x30,
0x30,0x30,0x00,0x00,0x00,0x3e,0x00,0x01,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x04,
0x06,0x00,0x80,0x00,0x3d,0x00,0x00,0x01,0xa0,0x00,0x80,0x00,0x4a,0x00,0x00,0x00,
0x00,0x00,0x00,0x01,0x00,0x00,0x0d,0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x77,0x6f,
0x72,0x6c,0x64,0x21,0x00,0x00,0x00,0x01,0x00,0x01,0x70,0x00,0x45,0x4e,0x44,0x00,
0x00,0x00,0x00,0x08,
};

ファイルロードしているところを得られた配列から読みこむようにすれば、
外部ファイルに依存しない単一バイナリとして成果物を用意することもできました。

// hello_from_arr.cpp
#include <iostream>
#include <memory>
#include <mruby.h>
#include <mruby/dump.h>

// テストなので雑にコンパイルされたファイルを組み込む
// 内部で stdint.h を複数回includeすることになるのでホントはNG
#include "./hello.c"

using namespace std;

int main( int argc, char** argv ) {
  // 仮想マシンを初期化して
  shared_ptr< mrb_state > mrb(mrb_open(), []( mrb_state* p ){ if ( p != nullptr ){ mrb_close(p); } } );
  // 実行する ("Hello, World!" が出る)
  mrb_load_irep(mrb.get(), hello);

  return 0;
}
# コンパイルして
clang++ -Wall -I../include -L../build/host/lib -o hello_from_arr hello_from_arr.cpp -lmruby -std=c++11

# 実行
./hello_from_arr
"Hello, world!"

正直にバイトコードをせこせこ読み込みたいなら下記のようにします。

// hello_from_raw.cpp
#include <iostream>
#include <fstream>
#include <memory>
#include <mruby.h>
#include <mruby/dump.h>

using namespace std;

int main( int argc, char** argv ) {
  // ifstream でコンパイル済みのコードを読み込む
  const char* fn = "hello.mrb";
  ifstream ifs;
  ifs.open( fn, ios::in|ios::binary );
  if ( !ifs ) {
    // エラー処理は程々に
    cout << "Load " << fn << " failed." << endl;
    return 1;
  }

  // サイズを取得して
  ifs.seekg(0, fstream::end);
  auto size = ifs.tellg();
  ifs.clear();
  ifs.seekg(0, fstream::beg);

  // バッファを用意してメモリに展開する
  auto buff = shared_ptr< uint8_t >( new uint8_t[size], std::default_delete<uint8_t[]>() );
  ifs.read( reinterpret_cast< char* >( buff.get() ), sizeof( uint8_t ) * size );
  ifs.close();

  // 仮想マシンを初期化して
  shared_ptr< mrb_state > mrb(mrb_open(), []( mrb_state* p ){ if ( p != nullptr ){ mrb_close(p); } } );
  // 実行する ("Hello, World!" が出る)
  mrb_load_irep(mrb.get(), buff.get());

  return 0;
}
# コンパイルして
clang++ -Wall -I../include -L../build/host/lib -o hello_from_raw hello_from_raw.cpp -lmruby -std=c++11

# 実行
./hello_from_raw
"Hello, world!"

これから

今回はC++からmrubyの処理系を用意してスクリプトを単に実行するまでをやりました。

より実践的に使うには、母体となる処理系で関数やクラスを用意してあげて、 mrubyから使ったり、あるいは逆に、mrubyで定義されたクラスや 関数を母体の方から実行したりと言ったバインディング機能を利用することが必要となるでしょう。

雑に思いつく使い方といえば、、、

といったところ。ぶっちゃけバインディングさえできれば、 Javascriptだろうが、Luaだろうが、Xtalだろうがこの辺りはなんでも良くて、 mrubyに限った話ではないというところではあるのですが。

スクリプトとして使いたい処理系の扱いやすさと、 実際の実行速度との天秤で選定していくことが必要になるのだと思います。

冒頭に上げた単一バイナリのツールを作ったりするのであれば、 mruby-cli というやつを使えばコマンドラインツールもさくっと作れるとのことらしい。 それはまた気が向いらた別の機会に。

参考コードはあくまで参考程度にどうぞ。(どこかメモリリークしてるかも)


関連リンク

今回の扉ネコ www.pakutaso.com