目次へ戻る

2. MRuby 探索

2012-05-18 (鈴)

1 はじめに

本章では組込み用 Ruby 処理系 mruby の作りと使い方を探索する。

執筆時現在,mruby はまだ正式にリリースされておらず, https://github.com/mruby/mruby から

$ git clone https://github.com/mruby/mruby.git

として最新の開発版を入手できる。 git [git-scm.com] をインストールしていない, あるいは通信経路・プロトコルの制限等により git が使えない環境でも同ページの Downloads リンクから HTTPS 経由で tar.gz ファイルをダウンロードできる。

現在,最新版の tar.gz ファイルの URL はリビジョンにかかわらず https://nodeload.github.com/mruby/mruby/tarball/master です。 しかし,(企業内からのアクセス経路にしばしば設置され,man in the middle として HTTPS の通信内容を平文でモニタするタイプの) HTTP プロキシを経由してダウンロードする場合は,プロキシで同一のリクエストであると判断されて, いつまでも同じ古いファイルがプロキシのキャッシュから返されることがあります。 そのような場合,バッド・ノウハウですが,例えば今が 5 月 10 日の夕方であると仮定してその時点での最新版 878593430a を取るには https://nodeload.github.com/mruby/mruby/tarball/8785934 のように URL を指定して mruby-mruby-8785934.tar.gz をダウンロードします。

本章の以下の記述では 5 月 17 日 朝の最新版 3bbd82c240 を https://github.com/mruby/mruby の Downloads リンクから mruby-mruby-3bbd82c.tar.gz としてダウンロードしたと仮定する。

01:~/tmp$ tar xf mruby-mruby-3bbd82c.tar.gz
01:~/tmp$ ln -s mruby-mruby-3bbd82c mruby
01:~/tmp$ cd mruby
01:~/tmp/mruby$  

2 構築

mruby は 32 ビットおよび 64 ビットの GNU ベースのコンパイル環境で make できる。 下記は Mac OS X 10.6.8 (Xcode 3.2.6, 32 ビット) でのコンパイル例である。 これに見るように,少なくとも配布時の設定では gcc [gnu.org] と bison [gnu.org] を必要とする。 また,その Makefile は,古典的な make コマンドを超える GNU make [gnu.org] の拡張機能を必要としている。

01:~/tmp/mruby$ make
gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I. -I./../include -
c array.c -o array.o
gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I. -I./../include -
c ascii.c -o ascii.o
……中略……
bison -o ./y.tab.c ./parse.y
gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I. -I./../include -
c ./y.tab.c -o ./y.tab.o
……中略……
gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I../../src -I../../
src/../include -c ../../src/../tools/mirb/mirb.c -o ../../src/../tools/mirb/mirb
.o
gcc -o ../../bin/mirb -g -O3 ../../src/../tools/mirb/mirb.o ../../lib/libmruby.a
  -lm
01:~/tmp/mruby$  

コンパイル結果は bin ディレクトリの下にあるそれぞれ正味 1MB 弱の三つの実行ファイルである。 下記では bin/mruby だけ strip しているが他の二つも strip すれば同程度の大きさになる。

01:~/tmp/mruby$ ls -l bin
total 6624
-rwxr-xr-x  1 suzuki  staff  1133020 May 17 08:24 mirb*
-rwxr-xr-x  1 suzuki  staff  1121076 May 17 08:24 mrbc*
-rwxr-xr-x  1 suzuki  staff  1132840 May 17 08:24 mruby*
01:~/tmp/mruby$ strip bin/mruby
01:~/tmp/mruby$ ls -l bin/mruby
-rwxr-xr-x  1 suzuki  staff  997020 May 17 08:35 bin/mruby*
01:~/tmp/mruby$ otool -L bin/mruby
bin/mruby:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version
 125.2.11)
01:~/tmp/mruby$  
otool -L は使う共有ライブラリを表示する Mac OS X (Darwin) 特有の方法です。Cygwin や Linux では ldd を使います。
01:~/tmp/mruby$ ldd bin/mruby

bin/mruby を実行してみよう。

01:~/tmp/mruby$ ./bin/mruby --version
ruby 1.8.7 (2010-08-16 patchlevel 302) [i386-mingw32]
Usage: ./bin/mruby [switches] programfile
  switches:
  -b           load and execute RiteBinary (mrb) file
  -c           check syntax only
  -e 'command' one line of script
  -v           print version number, then run in verbose mode
  --verbose    run in verbose mode
  --version    print the version
  --copyright  print the copyright
01:~/tmp/mruby$ ./bin/mruby -e 'p 5 + 6'
11
01:~/tmp/mruby$  
バージョン番号,リリース日付,プラットフォームに驚いたと思いますが,これは src/version.h に決め打ちで書かれているものが表示されているだけです。

3 構築過程再訪

ここで構築過程を振り返る。

最初の要点として src/ の各 *.c ファイルをコンパイルし終わったその次の箇所に注目しよう。 ここではパーサ・ジェネレータ bison を使って Ruby 言語の構文解析器 src/y.tab.csrc/parse.y から生成している。 src/y.tab.c をコンパイルして src/y.tab.o を作ったあと, それを含めた src/ の *.o ファイルをまとめてライブラリ lib/libmruby.a を作成している。

bison -o ./y.tab.c ./parse.y
gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I. -I./../include -
c ./y.tab.c -o ./y.tab.o
ar r ../lib/libmruby.a ./array.o ./ascii.o ./cdump.o ./class.o ./codegen.o ./com
par.o ./crc.o ./dump.o ./encoding.o ./enum.o ./error.o ./etc.o ./gc.o ./hash.o .
/init.o ./init_ext.o ./kernel.o ./load.o ./math.o ./numeric.o ./object.o ./pool.
o ./print.o ./proc.o ./range.o ./re.o ./regcomp.o ./regenc.o ./regerror.o ./rege
xec.o ./regparse.o ./sprintf.o ./st.o ./state.o ./string.o ./struct.o ./symbol.o
 ./time.o ./transcode.o ./unicode.o ./us_ascii.o ./utf_8.o ./variable.o ./versio
n.o ./vm.o   ./y.tab.o
ar: creating archive ../lib/libmruby.a

mruby の構築過程で最も興味深いのは,これに引き続く箇所である。

mrblib/ の下には Ruby 言語で書かれたクラス定義とモジュール定義が array.rb 等のファイルとして置かれている。 まず,これらをすべて連結して1本のファイル mrblib/mrblib.rbtmp を作成する。

cat array.rb compar.rb enum.rb error.rb hash.rb kernel.rb numeric.rb print.rb ra
nge.rb string.rb struct.rb > mrblib.rbtmp
組込みクラスとしての Array そのものは C 言語による include/mruby/array.h と src/array.c で定義され,実装されています。 mrblib/array.rb は,この Array に Ruby 言語でメソッドを追加定義します。 下記に mrblib/array.rb の冒頭部を示します。
##
# Array
#
# ISO 15.2.12
class Array

  ##
  # Calls the given block for each element of +self+
  # and pass the respective element.
  #
  # ISO 15.2.12.5.10
  def each(&block)
    idx = 0
    while(idx < length)
      block.call(self[idx])
      idx += 1
    end
    self
  end

次に,中間言語コンパイラとしての main 関数を含む tools/mrbc/mrbc.c と,さきほど作ったばかりの lib/libmruby.a から,1個の中間言語コンパイラ bin/mrbc を構築する。

gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I../../src -I../../
src/../include -c ../../src/../tools/mrbc/mrbc.c -o ../../src/../tools/mrbc/mrbc
.o
gcc -o ../../bin/mrbc ../../src/../tools/mrbc/mrbc.o ../../lib/libmruby.a -lm
中間言語 (intermediate language) については次節で説明します。

Ruby 言語で書かれた mrblib/mrblib.rbtmp-B オプション付きの bin/mrbc で中間言語コンパイルし,C 言語配列として表現された中間言語コードへと変換する。 -B オプションの引数は C 言語配列名,-o オプションの引数は出力ファイル名である。 そうして得られた出力ファイル mrblib/mrblib.ctmpmrblib/init_mrblib.c と連結して1本の mrblib/mrblib.c を作成する。 mrblib/init_mrblib.c には C 言語配列で表現された中間言語を仮想マシンに読み込ませて実行する C 言語関数が書かれている。

../bin/mrbc -Bmrblib_irep -omrblib.ctmp mrblib.rbtmp; cat init_mrblib.c mrblib.c
tmp > mrblib.c

こうして作成した mrblib/mrblib.c を (今度は普通の C 言語として) コンパイルして,結果の mrblib.olib/libmruby.a に追加する。 mruby の構築過程では,このようなブートストラップ的な手順によって,Ruby 言語で書かれたクラス定義とモジュール定義を,Ruby 言語を実装するライブラリ自身に中間言語コードのかたちで組み入れている。

gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I../src -I../includ
e -c mrblib.c -o mrblib.o
ar r ../lib/libmruby.a mrblib.o

bin/mruby と対話シェル bin/mirb は,それぞれの main 関数を含む tools/mruby/mruby.ctools/mirb/mirb.c と,上述のように中間言語コードを組み入れた lib/libmruby.a から構築されている。 bin/mrbc と異なり,実際に Ruby 言語を実行するこれらのプログラムは,組込みクラスと組込みモジュールの完全な定義を必要とするからである。

gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I../../src -I../../
src/../include -c ../../src/../tools/mruby/mruby.c -o ../../src/../tools/mruby/m
ruby.o
gcc -o ../../bin/mruby -g -O3 ../../src/../tools/mruby/mruby.o ../../lib/libmrub
y.a  -lm
gcc -Wall -Werror-implicit-function-declaration -g -O3 -MMD -I../../src -I../../
src/../include -c ../../src/../tools/mirb/mirb.c -o ../../src/../tools/mirb/mirb
.o
gcc -o ../../bin/mirb -g -O3 ../../src/../tools/mirb/mirb.o ../../lib/libmruby.a
  -lm
gcc に与えられた -lm オプションにより数学ライブラリ libm もリンクされます。 Cygwin ではその実体は /usr/lib/libm.a です。 静的リンクですから ldd による表示には出てきません。 一方,Mac OS X には libm.a がなく,libSystem.dylib へのシンボリック・リンクとしての libm.dylib があります。 結果として Mac OS X では共有ライブラリ libSystem だけが OS 由来のライブラリとして使われます。

4 中間言語コード

言語処理系としての mruby は python [python.org] や lua [lua.org] と同じく,与えられたソース・プログラムを中間言語コンパイラが仮想マシン用の中間言語へとコンパイルし, 仮想マシンがその中間言語コードの列を解釈することによって,プログラムを実行するように構成されている。 bin/mruby を実行したときのように,普段,この過程は1プロセス内で完結している。 しかし,毎回の起動時間の短縮のため,あるいは実行時の計算資源の節約のために, コンパイルした中間言語コードを保存して後で使うようにすることもできる。 mruby の mrbc コマンドと lua の luac コマンドはそのための専用のコンパイラである。 python は python コマンド自身がそうしたコンパイラを兼ねている。

ここで挙げた三つの処理系の中間言語に直接の互換性はありません。 それぞれ実装する言語の動作モデルやサポートするデータ型を効率良く実現するため, 専用に設計されたオリジナルの中間言語が使われています。
そうしたなかで興味深く,産業的に重要な例外の一つとして,組込み用 Python 処理系 Python-on-a-Chip [google.com] (以下 p14p) があります。 これは通常版の Python 処理系である python の中間言語のサブセットを使います。 事実,p14p はその中間言語コンパイラの主要な処理を python 自身の組込み関数 compile であっさりと簡単に実現しています。 そうしたことから,p14p のなかにあって,限られた計算資源のもとで中間言語を効率良く実行する PyMite 仮想マシンが p14p と半ば同義語のようになっています。
言語の性格と適用分野から p14p は mruby の直接のライバルないしカウンターパートと言えそうですが,通常版処理系との関係でみると,両者が統合的に運用される python/p14p と,互いに切り離され独立している ruby/mruby は対照的です。 PyMite 仮想マシンはすでに多くのプラットフォームに移植されており,フロントエンドの python とシリアル I/F を経由した対話的な開発環境を組込みボードで実現しています。 もしも今すぐ利用できる組込み用軽量言語を探しているならば,p14p は lua と並んで検討すべき良い候補です。 mruby にとっては超えなければならない目標の一つになりそうです。

前節で駆け足で取り上げた mrblib/mrblib.c を実際に見てみよう。 すでに述べたように,このファイルは Ruby 言語で書かれたライブラリをコンパイルして得られた中間言語コードを配列として保持している。 配列名は mrbc-B オプションで指定された mrblib_irep である。 前半の C 言語関数は mrblib/init_mrblib.c からのコピーである。

#include "mruby.h"
#include "mruby/irep.h"
#include "mruby/dump.h"
#include "mruby/string.h"
#include "mruby/proc.h"

extern const char mrblib_irep[];

void
mrb_init_mrblib(mrb_state *mrb)
{
  int n = mrb_read_irep(mrb, mrblib_irep);

  extern mrb_value mrb_top_self(mrb_state *mrb);
  mrb_run(mrb, mrb_proc_new(mrb, mrb->irep[n]), mrb_top_self(mrb));
}

const char mrblib_irep[] = {
0x52,0x49,0x54,0x45,0x30,0x30,0x30,0x39,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x39,
0x30,0x30,0x30,0x30,0x4d,0x41,0x54,0x5a,0x20,0x20,0x20,0x20,0x30,0x30,0x30,0x39,
0x30,0x30,0x30,0x30,0x00,0x00,0x36,0xf1,0x00,0x73,0x00,0x00,0x20,0x20,0x20,0x20,
……中略……
0x80,0x00,0x05,0x02,0x80,0x80,0xa0,0x02,0x80,0x00,0x05,0x02,0x80,0x00,0x29,0x55,
0x51,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x02,0x5b,0x5d,0x00,
0x04,0x63,0x61,0x6c,0x6c,0x00,0x04,0x70,0x75,0x73,0x68,0x3a,0x3a,0x00,0x00,0x00,
0x00,
};

この意味するところを調べるため,試しに mrbc を使って次の短い Ruby スクリプトを中間言語にコンパイルしてみよう。

p 5 + 6

前節で説明したように, -B オプションを使えば C 言語の配列としてコンパイル結果が得られる。 -o オプションで結果の出力先となるファイルを指定できる。

01:~/tmp$ cat p.rb
p 5 + 6
01:~/tmp$ ./mruby/bin/mrbc
Usage: ./mruby/bin/mrbc [switches] programfile
  switches:
  -c           check syntax only
  -o<outfile>  place the output into <outfile>
  -v           print version number, then trun on verbose mode
  -B<symbol>   binary <symbol> output in C language format
  -C<func>     function <func> output in C language format
  --verbose    run at verbose mode
  --version    print the version
  --copyright  print the copyright
2521:~/tmp$ ./mruby/bin/mrbc -Bfoo -obar.c p.rb
01:~/tmp$ cat bar.c
const char foo[] = {
0x52,0x49,0x54,0x45,0x30,0x30,0x30,0x39,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x39,
0x30,0x30,0x30,0x30,0x4d,0x41,0x54,0x5a,0x20,0x20,0x20,0x20,0x30,0x30,0x30,0x39,
0x30,0x30,0x30,0x30,0x00,0x00,0x00,0x80,0x00,0x01,0x00,0x00,0x20,0x20,0x20,0x20,
0x20,0x20,0x20,0x20,0xcb,0x22,0x00,0x00,0x00,0x42,0x53,0x43,0x00,0x02,0x00,0x05,
0x00,0x02,0x7f,0x09,0x00,0x00,0x00,0x08,0x01,0x00,0x00,0x06,0x01,0xc0,0x02,0x03,
0x02,0x40,0x02,0x83,0x02,0x80,0x00,0x05,0x01,0x80,0x40,0xac,0x02,0x00,0x00,0x05,
0x01,0x00,0x00,0xa0,0x00,0x00,0x00,0x4a,0x79,0x62,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x02,0x00,0x01,0x70,0x00,0x01,0x2b,0x7a,0xc1,0x00,0x00,0x00,0x00,
};
01:~/tmp$  

-v オプションを使えばバージョン番号とコンパイルの過程を見ることができる。 与えたスクリプトに対する抽象構文木 (abstract syntax tree) と,結果としての中間言語コードのアセンブリ言語表記が表示される。 この抽象構文木は字下げで入れ子を表現している。

01:~/tmp$ ./mruby/bin/mrbc -v p.rb 
ruby 1.8.7 (2010-08-16 patchlevel 302) [i386-mingw32]
NODE_SCOPE:
  local variables:
  NODE_BEGIN:
    NODE_CALL:
      NODE_SELF
      method='p' (249)
      args:
        NODE_CALL:
          NODE_INT 5 base 10
          method='+' (76)
          args:
            NODE_INT 6 base 10
irep 0 nregs=5 nlocals=2 pools=0 syms=2
000 OP_LOADSELF	R2
001 OP_LOADI	R3	5
002 OP_LOADI	R4	6
003 OP_LOADNIL	R5
004 OP_ADD	R3	'+'	1
005 OP_LOADNIL	R4
006 OP_SEND	R2	'p'	1
007 OP_STOP

01:~/tmp$  

mrb は,-B オプション等がないとき,結果を改行なしの ASCII テキストとして出力する。 -o オプションがないとき,元のファイル名の接尾辞を変更したファイル名を出力先として使う。 つまり,上記の実行の結果として下記のような (p.rb の接尾辞を変更した名前をもつ) ファイル p.mrb が作られている。

01:~/tmp$ file p.mrb
p.mrb: ASCII text, with no line terminators
01:~/tmp$ cat p.mrb
RITE0009000000090000MATZ    000900000000008000010000        CB2200000080SC000200
0500047F09000000080100000601C002030240028302800005018040AC02000005010000A0000000
4A7962000000000000000000020001p0001+7AC10000000001:~/tmp$  
これを -B オプションで出力したファイル bar.c の配列 foo の内容と比較してください。 配列内容のバイナリ・データが,少し自明でない方法で,制御文字を含まないテキストにエンコードされています。 この形式は,例えば,そのままフロー制御込みでファイル内容をデバイスに転送するのに便利そうです。
しかし,たとえ転送の便宜を考えてこうしたのだとしても,例えば,サイズを節約するために簡単な圧縮を施すことを考えれば, この段階では単純なバイナリ・ダンプとしたほうが正着だったかもしれません。 早い段階で特定の環境での特定の使い方に特化して表現形式を設計すると,後々無駄や不合理を強いられることになりがちですが,今の mruby はその轍を踏んでいるように思えます……。

mruby はこの形式のファイルを直接読み込んで実行することができる。

01:~/tmp$ ./mruby/bin/mruby -b p.mrb
11
01:~/tmp$  

bar.cp.mrb がそれぞれの表現形式で格納している中間言語コードは,-v オプションを与えたときに表示されたアセンブリ言語に直接対応する狭義の中間言語の命令列に加えて, 多くの補助的な情報を含んでいる。 しかるに,-C オプションを与えたときは,補助的な情報が C 言語関数内の処理に埋め込まれて表現されるから,中間言語とアセンブリ言語との対応が分かりやすい。

01:~/tmp$ ./mruby/bin/mrbc -Cbaz p.rb
01:~/tmp$  

結果として得られる p.c の内容を下記に示す。 配列 iseq_0 の内容 01 00 00 06 から 00 00 00 00 4a までが,(-v オプションで表示されたアセンブリ言語に直接対応する) 狭義の中間言語コードの列 (sequence) である。

対応する部分を bar.cp.mrb から探してみてください。すぐ見つかります。
#include "mruby.h"
#include "mruby/irep.h"
#include "mruby/string.h"
#include "mruby/proc.h"

static mrb_code iseq_0[] = {
  0x01000006,
  0x01c00203,
  0x02400283,
  0x02800005,
  0x018040ac,
  0x02000005,
  0x010000a0,
  0x0000004a,
};

void
baz(mrb_state *mrb)
{
  int n = mrb->irep_len;
  int idx = n;
  mrb_irep *irep;

  mrb_add_irep(mrb, idx+1);

  irep = mrb->irep[idx] = mrb_malloc(mrb, sizeof(mrb_irep));
  irep->idx = idx++;
  irep->flags = 0 | MRB_ISEQ_NOFREE;
  irep->nlocals = 2;
  irep->nregs = 5;
  irep->ilen = 8;
  irep->iseq = iseq_0;
  irep->slen = 2;
  irep->syms = mrb_malloc(mrb, sizeof(mrb_sym)*2);
  irep->syms[0] = mrb_intern(mrb, "p");
  irep->syms[1] = mrb_intern(mrb, "+");
  irep->plen = 0;
  irep->pool = NULL;

  mrb->irep_len = idx;

  extern mrb_value mrb_top_self(mrb_state *mrb);
  mrb_run(mrb, mrb_proc_new(mrb, mrb->irep[n]), mrb_top_self(mrb));
}

上記 p.c の要点を下記に述べる。

この p.c を本節の最初で見た mrblib/mrblib.c の前半部 (つまり mrblib/init_mrblib.c) と比較してください。 何を引数に与えて何が得られているかに注意して読むと, -B オプションによる配列として表現された広義の中間コードが,Ruby スクリプトの内部表現である mrb_irep 構造体を構築するために十分なだけの情報を含んでいることが分かります。 p.c では同じ情報が mrb_irep 構造体の各メンバへ代入される値として記述されているわけです。

5 おわりに

しめくくりとして,中間言語コンパイラ mrbc が生成したコードを実際に実行する C 言語プログラムを書いてみよう。

前節の最後で mrbc -Cbaz p.rb によって生成した C 言語ファイル p.c は,-C オプションの引数で指定した名前 baz をもつ外部関数を定義している。 下記のように mrb_state 構造体を構築して関数 baz に引き渡すプログラムを作る。

#include "mruby.h"

extern void baz(mrb_state* mrb);

int main()
{
    mrb_state* mrb = mrb_open();
    baz(mrb);
    mrb_close(mrb);
    return 0;
}

この C 言語ファイルを p-main.c と名付ける。 このとき,次のようにコンパイルし,実行できる。 Ruby 言語の p 5 + 6 にあたる中間言語コードが実行され,11 が表示される。

01:~/tmp$ gcc -Imruby/include -Imruby/src p-main.c p.c -Lmruby/lib -lmruby
01:~/tmp$ ./a.out
11
01:~/tmp$  
本来は -I オプションで mruby/src を include ファイルの検索パスに追加する必要はないはずですが……。

p.mrb を読み込んで実行するようなプログラムの作り方については mruby のソースである tools/mruby/mruby.c を参照されたい。

要点を抜き書きすれば次のようになります。 本章で mrb_run 関数が出てきたのはこれが3度目であることに注意してください。 どれも基本的には同じ方法です。
  mrb_state *mrb = mrb_open();
  int n = -1;

    n = mrb_load_irep(mrb, args.rfp);

      mrb_run(mrb, mrb_proc_new(mrb, mrb->irep[n]), mrb_top_self(mrb));

Copyright (c) 2012 OKI Software Co., Ltd.