====== 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
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)。
- rite_file_header (64-byte)
- ( レコード長(8-byte) + irepレコード ) × header.nirep
- ダミーレコード長(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□□□□" ((□は半角スペース))| |
|rcv |Rite Compiler Version | 8|"00090000"| |
|rbds |Rite Binary Data Size | 8|"000000C5"| |
|nirep|Number of ireps | 4|"0002" | |
|sirep|Start 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 || 8×//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"| |
|0|symbol 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 || 8×//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"| |
|0|symbol 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□□□□" ((□は半角スペース))| |
|rcv |Rite Compiler Version | 8|"00090000"| |
|rbds |Rite Binary Data Size | 4|0x000000C5|バイナリに変換|
|nirep|Number of ireps | 2|0x0002 |バイナリに変換|
|sirep|Start index | 2|0x0000 |バイナリに変換|
|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_class^ridx^eidx^stackidx ^pc ^acc^env ^stack^
|OP_ONERR sBx | | | | | | |++ | | | | | | |
|OP_POPERR A | | | | | | |-= A| | | | | | |
|OP_RAISE A | | | | | | | | | | | | | |
|OP_EPUSH Bx | | | | | | | |++ | | | |Proc(SEQ[Bx])->env((なければ新規作成@closure_setup))| |
|OP_EPOP A\\ (右記をA回繰り返す)|++ci|不変 |p=ensure[i] |p->body.irep->nregs |0 |p->target_class|不変|--|stack-stbase|無効((代入してない無効値。使われることがないため))|-1|NULL|+= ci[-1].nregs|
|OP_SEND A,B,C\\ OP_SENDB A,B,C|++ci|Sym(B)|Sym(B)のproc|C+2((ただしC==MAXARGSのときは3))(CFUNCの場合)\\ irep->nregs(非CFUNCの場合)|C((ただしC==MAXARGSのときは-1))|R(A)->c |不変((ci[-1]からそのままコピー。以下同。))|不変|stack-stbase|pc+1|A |NULL|+= A |
|OP_SUPER A,C |::: |ci[-1]->mid|ci[-1]->midのproc|2((これはおそらくバグと思われる。OP_SENDと同じのはず…))(CFUNCの場合)\\ irep->nregs(非CFUNCの場合)|C((ただしC==MAXARGSのときは-1))|m->target_class|:::|:::|:::|:::|:::|:::|:::|
|OP_CALL | |m->env->mid|m | | |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)を親とし((親をe->cに入れているのがややこしい。->cをクラスではなく親へのリンクとして共用しているだけ。))(NULLの場合もある)、flagsに現在のローカル変数数、mid、ciの数、stack位置を記録する。ci->envは新envで更新される。
ci->envが作成済みの場合は、proc->envにci->envがセットされるだけである。要するに、UPVARで戻って参照される場合のみ、envを作るようになっている。(パフォーマンスのためかな?)
つまりは、ブロックを渡す側で自分のstackにアクセスできるようproc->envを設定してやるようになっている。