[[mrubyを読む]] #contents *はじめに [#bdd53068] コードも生成できたので最後にコードを実行している部分を読みます。mrb_run()がエントリポイントになります。 *mrubyVM概観 [#p63559f8] いきなりmrb_run()に入る前にmrubyVM((Riteはコードネームだから今後はmrubyと呼んで、とまつもとさんがつぶやいてたのでRiteVMと呼ばずにmrubyVMと呼びます))がどんな実行モデルなのかについて説明します。 mrubyVMの実行モデルはレジスタマシンです。ちなみにYARVはスタックマシンです。レジスタマシンとスタックマシンの違いはWikipediaあたりをご参照ください。 例えば以下の単純なRubyスクリプトの場合、 def foo(a, b) a * b end f = foo(1, 2) irep 116 nregs=6 nlocals=3 pools=0 syms=1 000 OP_TCLASS R3 001 OP_LAMBDA R4 I(117) 1 002 OP_METHOD R3 'foo' 003 OP_LOADSELF R3 004 OP_LOADI R4 1 005 OP_LOADI R5 2 006 OP_LOADNIL R6 007 OP_SEND R3 'foo' 2 008 OP_MOVE R1 R3 009 OP_STOP irep 117 nregs=7 nlocals=5 pools=0 syms=1 000 OP_ENTER 2:0:0:0:0:0:0 001 OP_MOVE R5 R1 002 OP_MOVE R6 R2 003 OP_LOADNIL R7 004 OP_SEND R5 '*' 1 005 OP_RETURN R5 f = foo(1, 2)の部分は以下のように実行されます。 +レジスタR3にselfをロード(レシーバを設定) +レジスタR4に1をロード(引数を設定) +レジスタR5に2をロード(引数を設定) +レジスタR6にnilをロード(ブロック引数を設定) +レジスタR3のオブジェクトに対して'foo'メソッドを引数が2つで実行 +レジスタR1(ローカル変数f)にメソッド呼び出しの結果(R3に格納されます)を設定 mrubyではレジスタの確保場所としてスタックを使用しています。そのため、mrubyVMはスタックマシンであると勘違いしてしまう危険があるので注意してください。さわだもソースだけ見ていてスタックマシンだと勘違いしていました。 レジスタの確保場所としてスタックを使うとはどういうことかというと、以下のようなイメージです。(OP_SEND '*'実行直前の状態) トップレベル実行時のスタックベース→| nil |top_self | |ローカル変数fの格納領域 | nil |よくわからない。特殊変数用? メソッドfoo実行時のスタックベース→| nil |'foo'のレシーバ | 1 |'foo'の引数1 & ローカル変数a | 2 |'foo'の引数2 & ローカル変数b | nil |'foo'に対するブロック引数 | nil |よくわからない。特殊変数用? | 1 |'*'のレシーバ | 2 |'*'の引数1 | nil |'*'に対するブロック引数 以上の前提を持ってmrb_run()に挑むと理解が深まると思います。 *mrb_run(src/vm.c) [#s87bb89e] では、mrb_run()に見ていくことにしましょう。mrb_run()は一言で言うと一つ一つ命令を実行するループと各命令の処理に振り分ける巨大なswitch文です。ただし、処理効率化のためにちょっとカラクリが施されています。 #code(C){{ INIT_DISPACTH { CASE(OP_NOP) { /* do nothing */ NEXT; } CASE(OP_MOVE) { /* A B R(A) := R(B) */ int a = GETARG_A(i); int b = GETARG_B(i); regs[a].tt = regs[b].tt; regs[a].value = regs[b].value; NEXT; } ... } END_DISPACTH; }} INIT_DISPACTH((スペルミスだと思うのだけど、何故修正されないのだろう?)), CASE, NEXTの定義はmrb_run()の少し上に書かれています。 #code(C){{ #ifdef __GNUC__ #define DIRECT_THREADED #endif #ifndef DIRECT_THREADED #define INIT_DISPACTH for (;;) { i = *pc; switch (GET_OPCODE(i)) { #define CASE(op) case op: #define NEXT mrb->arena_idx = ai; pc++; break #define JUMP break #define END_DISPACTH } } #else #define INIT_DISPACTH JUMP; return mrb_nil_value(); #define CASE(op) L_ ## op: #define NEXT mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)] #define JUMP i=*pc; goto *optable[GET_OPCODE(i)] #define END_DISPACTH #endif }} gccかどうかで定義が変ってます。めんどくさいけどちゃんとマクロ展開されたコードを示すことにします。 gccじゃない場合 #code(C){{ for (;;) { i = *pc; switch (GET_OPCODE(i)) { { case OP_NOP: { /* do nothing */ mrb->arena_idx = ai; pc++; break; } case OP_MOVE: { /* A B R(A) := R(B) */ int a = GETARG_A(i); int b = GETARG_B(i); regs[a].tt = regs[b].tt; regs[a].value = regs[b].value; mrb->arena_idx = ai; pc++; break; } ... } } }; }} gccの場合 #code(C){{ i=*pc; goto *optable[GET_OPCODE(i)]; return mrb_nil_value(); { L_OP_NOP: { /* do nothing */ mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)]; } L_OP_MOVE: { /* A B R(A) := R(B) */ int a = GETARG_A(i); int b = GETARG_B(i); regs[a].tt = regs[b].tt; regs[a].value = regs[b].value; mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)]; } ... } ; }} というわけでgccじゃない場合は無限ループ & switch文ですが、gccの場合は命令コードで決まるジャンプ先に直接飛んでいます。こうすることで命令ごとに条件分岐をするコストがなくなるため高速化が実現できます。YARVでも同じことが行われていました。 *実行してみる [#n89a9da1] 総論は終わったので後は各論、いつものように各命令についてどのような処理が行われているかを見ていくことにします。なお、例外処理関係は別で扱う予定なので飛ばします。 **OP_LOADSELF [#dc95f7c0] mrb_run()を上から見ていくと初めはLOAD系の命令が並んでいます。その中でややわかりにくいのがLOADSELFだと思います。 #code(C){{ CASE(OP_LOADSELF) { /* A R(A) := self */ regs[GETARG_A(i)] = mrb->stack[0]; NEXT; } }} 何でこれでselfを設定したことになるのか?というと、スタックベースにはselfのオブジェクトが格納されているからです。上に書いたスタックのイメージを再掲すると、 メソッドfoo実行時のスタックベース→| nil |'foo'のレシーバ | 1 |'foo'の引数1 & ローカル変数a | 2 |'foo'の引数2 & ローカル変数b | nil |'foo'に対するブロック引数 というわけでstack[0]がselfになっていることがご理解いただけると思います。 このstack[0]がselfであるということは他でも使われているので覚えておくようにしてください。例えば以下のようにインスタンス変数を取得するコードでもしれっと使われています。 #code(C){{ CASE(OP_GETIV) { /* A Bx R(A) := ivget(Bx) */ regs[GETARG_A(i)] = mrb_vm_iv_get(mrb, syms[GETARG_Bx(i)]); NEXT; } }} src/variable.c #code(C){{ mrb_value mrb_vm_iv_get(mrb_state *mrb, mrb_sym sym) { /* get self */ return mrb_iv_get(mrb, mrb->stack[0], sym); } }} **OP_SEND [#gc4bdea0] 次にメソッド呼び出しを行うOP_SENDを見てみましょう。長いので例によって区切って解説します。 #code(C){{ CASE(OP_SEND) { /* A B C R(A) := call(R(A),Sym(B),R(A+1),... ,R(A+C-1)) */ int a = GETARG_A(i); int n = GETARG_C(i); struct RProc *m; struct RClass *c; mrb_callinfo *ci; mrb_value recv; mrb_sym mid = syms[GETARG_B(i)]; }} コメントに書いてあるように、OP_SENDは3引数型の命令でそれぞれ以下の内容になっています。 :第1引数|メソッドのレシーバが格納されているレジスタ :第2引数|メソッドのシンボルテーブルへのインデックス番号 :第3引数|メソッドに渡す引数の数。引数はレシーバが格納されているレジスタの次のレジスタからn個格納する と、これがmrubyのメソッド呼び出し規約となっているようです。スタックイメージを再々掲すると、 | nil |R(A) 'foo'のレシーバ | 1 |R(A+1) 'foo'の引数1 | 2 |R(A+2) 'foo'の引数2 となっており、適切に呼び出す準備がされていることがわかります。((ところでコメントだと引数レジスタの末尾がR(A+C-1)になってるけど、R(A+C)では?)) 先に進みます。 #code(C){{ recv = regs[a]; c = mrb_class(mrb, recv); m = mrb_method_search_vm(mrb, &c, mid); if (!m) { mrb_value sym = mrb_symbol_value(mid); mid = mrb_intern(mrb, "method_missing"); m = mrb_method_search_vm(mrb, &c, mid); if (n == CALL_MAXARGS) { mrb_ary_unshift(mrb, regs[a+1], sym); } else { memmove(regs+a+2, regs+a+1, sizeof(mrb_value)*(n+1)); regs[a+1] = sym; n++; } } }} 呼び出すメソッドの検索をしています。メソッドがない場合はmethod_missingを呼ぶようにしています。 #code(C){{ /* push callinfo */ ci = cipush(mrb); ci->mid = mid; ci->proc = m; ci->stackidx = mrb->stack - mrb->stbase; ci->argc = n; if (ci->argc == CALL_MAXARGS) ci->argc = -1; ci->target_class = m->target_class; ci->pc = pc + 1; }} 呼び出し情報はmrb_callinfo構造体に格納されるようです。ここで覚えておくといいのはstackidxとpcがメソッドから返ってきたときの復元に使われる情報ということでしょうか。 #code(C){{ /* prepare stack */ mrb->stack += a; }} スタックベースを調整してレシーバが入っているスタック位置をstack[0]、すなわちselfになるようにしています。 #code(C){{ if (MRB_PROC_CFUNC_P(m)) { mrb->stack[0] = m->body.func(mrb, recv); mrb->arena_idx = ai; if (mrb->exc) goto L_RAISE; /* pop stackpos */ mrb->stack = mrb->stbase + ci->stackidx; cipop(mrb); NEXT; } }} メソッドがCで書かれている場合です。Cで書かれたメソッドの処理はまた別に見ます。 #code(C){{ else { /* fill callinfo */ ci->acc = a; /* setup environment for calling method */ proc = mrb->ci->proc = m; irep = m->body.irep; pool = irep->pool; syms = irep->syms; ci->nregs = irep->nregs; if (ci->argc < 0) { stack_extend(mrb, (irep->nregs < 3) ? 3 : irep->nregs, 3); } else { stack_extend(mrb, irep->nregs, ci->argc+2); } regs = mrb->stack; pc = irep->iseq; JUMP; } } }} メソッドがRubyで書かれている場合はこちらが実行されます。mrb_callinfo.accは先に書いてしまうとメソッドの戻り値をどこに入れるかの情報です。第1引数が保存されているのでレシーバを設定したレジスタに戻り値が格納されることがわかります。その後、各種実行時情報を呼び出し先のものに切り替えています。注意が必要なのはここでは呼び出すだけでメソッドから返ってくるのはOP_RETURNが実行されたときであるということです。 (以下、執筆中) *OP_ENTER [#g17feea3] メソッド先頭での引数処理 *OP_RETURN [#haf5ad3c] メソッドからの復帰 *OP_BLKPUSH [#i8132f88] yieldで使われる *OP_CALL [#de043443] yieldで使われる。OP_SEND("call")→OP_CALLという流れ *OP_ADD [#a323c777] 数値、文字列の場合は直接処理、それ以外はOP_SENDへ