mruby

mrubyのソースコード(https://github.com/mruby/mruby/)をざっと眺めてみて、気づいたことをメモ。

たどり方としては、tools/mruby/mruby.c から開き、ぱっと見で気になった所から掘り下げてみていく感じで。

データ型

mrb_value

typedef struct mrb_value {
  union {
    mrb_float f;
    void *p;
    mrb_int i;
    mrb_sym sym;
  } value;
  enum mrb_vtype tt:8;
} mrb_value;

なんてこった!VALUEやめちゃったのか。Fixnumを«1して保存するやり方、上手いなあと思っていたけどmrubyでは採用しなかったみたい。 とすると、よりRiteVMのHW化が生きてきそう。

構文解析

mrbc_filename @ parse.y

mrbc_contextにfilenameとlineno(=1)をセットするだけ。

見出し

メモリ管理

mrb_alloca @ state.c

allaca_headerのサイズを足してmrb_mallocしたブロック。alloca_headerは一方方向のリンクリストでnextポインタを持つだけ。 mrb→memsが最終要素のポインタ(ブロック先頭ではなくデータ領域先頭)を保持している。

mrb_malloc @ gc.c

NULLからのmrb_reallocするだけ。

mrb_realloc @ gc.c

mrb→allocfに任せる。ただし、失敗した場合はmrb_garbage_collectしてから一度だけ再試行する。

mrb_calloc @ gc.c

これはそのまんま。reallocしてmemset。

mrb_free @ gc.c

これもmrb→allocfに任せる。サイズ0へのreallocとして実行される。

RITEファイル構造

データ例に使ったテストコード

test.rb
# test.rb
10.times {|n| puts n }

基本構造

RITEファイル(*.mrb)は、ほとんどテキストファイルである。数値等は16進数表記されている。つまり整数値をそのまま16進表記にしたもので、プレフィックス(0x)やサフィックス(h)は付けない。 使える文字は [0-9A-Fa-f] である。それ以外の数値は 0 として扱われる(hex2bin @ load.c)。 ファイル中の改行コード(\r,\n)は無視される。また、#から\r,\n,\0まではコメントとして無視される(ignorecomment != 0の場合)。 なお、コメント終端の\0は非コメントのデータ扱い。コメント終端の\rや\nは前述どおり無視(rite_fgetc @ load.c)。

  1. rite_file_header (64-byte)
  2. ( レコード長(8-byte) + irepレコード ) × header.nirep
  3. ダミーレコード長(8-byte、値は0x00000000)

ヘッダ(rite_file_header)

バイナリヘッダ(rite_binary_header)とは微妙に異なるので混同注意!

メンバ名サイズデータ例説明
rbfi Rite Binary File Identify 4“RITE” 固定
rbfv Rite Binary File Format Version 8“00090000”固定
risv Rite Instruction Specification Version 8“00090000”
rct Rite Compiler Type 8“MATZ□□□□” 1)
rcv Rite Compiler Version 8“00090000”
rbds Rite Binary Data Size 8“000000C5”
nirepNumber of ireps 4“0002”
sirepStart index 4“0000”
rsv Reserved 8“□□□□□□□□”
hcrc HCRC 4“2BBD”
Total Size 64

irepレコード情報(0)

名前サイズデータ例説明
record len 8“0000005F”
IREP HEADER BLOCK
record identifier 1“S” 固定
class or module 1“C”
num of local variables 4“0001”
num of register variables 4“0003”
offset of isec block 4“0004”
header CRC 4“469F”
class or module name offset-4長さ0
ISEQ BLOCK
iseq length (blocklen) 8“00000004”
iseq blocklen“00C00483”
“01000340”
“00800021”
“0000004A”
LOADI; R(1) := 10
LAMBDA; R(2) := lambda(SEQ[0+1], OP_L_CAPTURE)
SENDB; R(1) := call(R(1), mSym(0), &R(2))
STOP;
iseq CRC 4“61F6”
POOL BLOCK
pool length (blocklen) 8“00000000”
TT (2)
pool data length (pdl) (4)
pool (pdl)
×blocklen
pool CRC 4“0000”
SYMS BLOCK
syms length (blocklen) 8“00000001”
0symbol name length (snl) (4)“0005”
symbol name (snl)“times”
×blocklen
syms CRC 4“94E7”

irepレコード情報(1)

名前サイズデータ例説明
record len 8“00000066”
IREP HEADER BLOCK
record identifier 1“S” 固定
class or module 1“C”
num of local variables 4“0003”
num of register variables 4“0005”
offset of isec block 4“0004”
header CRC 4“4839”
class or module name offset-4長さ0
ISEQ BLOCK
iseq length (blocklen) 8“00000005”
iseq blocklen“02000026”
“01800006”
“02004001”
“018000A0”
“01800029”
ENTER(1:0:0:0:0:0:0);
LOADSELF; R(3) := self
MOVE; R(4) := R(1)
SEND; R(3) := call(R(3), mSym(0), R(4))
RETURN; return R(3) [R_NORMAL]
iseq CRC 4“FA4E”
POOL BLOCK
pool length (blocklen) 8“00000000”
TT (2)
pool data length (pdl) (4)
pool (pdl)
×blocklen
pool CRC 4“0000”
SYMS BLOCK
syms length (blocklen) 8“00000001”
0symbol name length (snl) (4)“0004”
symbol name (snl)“puts”
×blocklen
syms CRC 4“2489”

ダミーirepレコード情報

名前サイズデータ例説明
record len 8“00000000”
EOF

RITEバイナリデータ構造

上記RITEファイルをバイナリに変換したもの。コメントを除去したり、16進表記の部分を変換したりするだけで、mrbファイルと大差はない。 なお、16進数表記はそのままの並びでバイナリに変換される。つまり、ビッグエンディアン配置となる。

ヘッダ(rite_binary_header)

ファイルヘッダ(rite_file_header)とは微妙に異なるので混同注意!

メンバ名サイズデータ例説明
rbfi Rite Binary File Identify 4“RITE” 固定
rbfv Rite Binary File Format Version 8“00090000”
risv Rite Instruction Specification Version 8“00090000”固定
rct Rite Compiler Type 8“MATZ□□□□” 2)
rcv Rite Compiler Version 8“00090000”
rbds Rite Binary Data Size 40x000000C5バイナリに変換
nirepNumber of ireps 20x0002 バイナリに変換
sirepStart index 20x0000 バイナリに変換
rsv Reserved 8“□□□□□□□□”
Total Size 52

なお、file_header.hcrcはこの構造体全体のCRC16(CCITT)である。

irepレコード情報(0)

TBW

RiteVM実行手順

まだ頭の中でまとめられていない。ここはメモ的なもの。まとまったら書き直す。

  • R0は常にselfを指してる。
  • irepはブロック、サブルーチンなどの単位で切り取られた一連の命令の塊。
  • irep内のどこを実行しているか?というプログラムカウンタが存在する。
  • Specialはまだ実装されていない。readするとFixnumの0になり、writeは無視される扱いのようだ。
  • mrb→ciは今実行中のブロックについて、そのコールスタック情報にあたるものっぽい。
    • たとえば 1.abs を実行中の場合、R0は'1'のインスタンスそのもの、ci→proc→target_classはclass Fixnumとなる。
      • じゃあ ci→target_classはなんなんさ??

callinfoとは

現在居るメソッドやブロックの情報を保持する構造体。スタックフレーム情報みたいなもの。 この構造体自体もスタック構造で管理されている。(cipush関数、cipop関数参照)

今居るブロックの情報と、戻り先の情報がまぜこぜになっていて非常にややこしいので整理する。

  • 今居るブロックに関する情報
    • mrb_sym mid : メソッドの名称(ブロックのときはNULL)
    • struct RProc* proc : メソッド/ブロック本体へのポインタ
    • int nregs : 今のブロックで使用するレジスタの個数(self含む)
      • cipushでは、初期値として2が設定される。これはmethod_missingで最低限使う分のようだ。
    • int argc : 今のブロックに与えられた引数の個数(配列渡しの場合は-1)
    • struct RClass* target_class : レシーバのクラス
      • proc→target_classとは異なる場合もある。たとえば 1.odd の場合、ci→target_classはレシーバそのもの(1:Fixnum)であるが、proc(:odd)→target_classはメソッドを持っているクラス(Integer)となる。
    • int ridx : rescueスタックの現在位置。最初は1つ前のciと同じ値から始まる。
    • int eidx : ensureスタックの現在位置。最初は1つ前のciと同じ値から始まる。
  • 戻る際に必要となる、復帰のための情報
    • int stackidx : 今のブロックに入る直前の、スタック先頭位置のインデックス。
    • mrb_code* pc : 今のブロックから抜けたときに戻る位置。
    • int acc : 戻った際に、戻り値を格納するレジスタのインデックス。
  • 不明
    • struct REnv* env : TODO 下記「envとは」を参照。

変更されるタイミングで整理すると、以下の通りになる。

operation ci ci→
mid proc nregs argc target_classridxeidxstackidx pc accenv stack
OP_ONERR sBx ++
OP_POPERR A -= A
OP_RAISE A
OP_EPUSH Bx ++ Proc(SEQ[Bx])→env3)
OP_EPOP A
(右記をA回繰り返す)
++ci不変 p=ensure[i] p→body.irep→nregs 0 p→target_class不変--stack-stbase無効4)-1NULL+= ci[-1].nregs
OP_SEND A,B,C
OP_SENDB A,B,C
++ciSym(B)Sym(B)のprocC+25)(CFUNCの場合)
irep→nregs(非CFUNCの場合)
C6)R(A)→c 不変7)不変stack-stbasepc+1A NULL+= A
OP_SUPER A,C ci[-1]→midci[-1]→midのproc28)(CFUNCの場合)
irep→nregs(非CFUNCの場合)
C9)m→target_class
OP_CALL m→env→midm m→target_class

envとは

前述のcallinfoはメソッドやブロックの呼び出しをそのままコールツリーとして記録するものだが、envは「ソースコード上の見た目のツリー」を記録するためのもの。 主な使い道はUPVARである。要するに、ブロックから上位のブロック/メソッドにある変数へ直接アクセスする際のアクセス経路を提供するもの。例を使って説明する。

class Array
  def each(&block)
    n = 0
    while (n < self.size)
      block.call(self[n])
    end
  end
end
def hoge
  total = 0         # R2
  [1,2,3].each {|v| # R3=[R3,R4,R5], R4=blockA
    # blockA        # R4=v, R3=total
    total += v      # R3=R3+R4, total=R3, return R3
  }
  total
end

この例の場合、callinfoは以下のようになる(Proc#callが0段として)。

level type name
0 method Object.hoge
1 method [1,2,3].each
2 block blockA

一方のenvは、クロージャの作成(closure_setup)のタイミングで生成され、ツリーが形成される。 クロージャの生成は、OP_LAMBDA(OP_L_CAPTUREが付いている場合)またはOP_EPUSHにて行われる。 ci→envが未作成の場合、クロージャ生成で新しいenvが作られる。現在のenv(ci→proc→env)を親とし10)(NULLの場合もある)、flagsに現在のローカル変数数、mid、ciの数、stack位置を記録する。ci→envは新envで更新される。 ci→envが作成済みの場合は、proc→envにci→envがセットされるだけである。要するに、UPVARで戻って参照される場合のみ、envを作るようになっている。(パフォーマンスのためかな?)

つまりは、ブロックを渡す側で自分のstackにアクセスできるようproc→envを設定してやるようになっている。

1) , 2)
□は半角スペース
3)
なければ新規作成@closure_setup
4)
代入してない無効値。使われることがないため
5)
ただしC==MAXARGSのときは3
6) , 9)
ただしC==MAXARGSのときは-1
7)
ci[-1]からそのままコピー。以下同。
8)
これはおそらくバグと思われる。OP_SENDと同じのはず…
10)
親をe→cに入れているのがややこしい。→cをクラスではなく親へのリンクとして共用しているだけ。
ruby/mruby.txt · 最終更新: 2013/04/14 10:39 (外部編集)
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0