ここではYARVコードへのコンパイルで生成したYARVコードを実行する処理を読解したいと思います。
YARVコード実行のエントリーポイントとなるのはrb_iseq_eval関数ですがこの関数はrb_vm_set_stack_top関数を呼んだ上でvm_eval_body関数を呼んでいるだけです。
vm_set_stack_top関数ではまずrb_vm_set_finish_env関数を呼んでフレームを生成しています。どうやらこのフレームはfinish命令を実行してYARVを終了するためのものなようです。
次にvm_push_frame関数を呼んで実行するYARVコードの情報をフレームに積んでいます。vm_push_frame関数は初期化のときもちらっと見ましたが再掲します。
vm_push_frame(th, iseq, FRAME_MAGIC_TOP, th->top_self, 0, iseq->iseq_encoded, th->cfp->sp, 0, iseq->local_size);
vm_push_frame(rb_thread_t *th, rb_iseq_t *iseq, VALUE type, VALUE self, VALUE specval, VALUE *pc, VALUE *sp, VALUE *lfp, int local_size) { ... /* nil initialize */ for (i=0; i < local_size; i++) { *sp = Qnil; sp++; } /* set special val */ *sp = GC_GUARDED_PTR(specval); dfp = sp; if (lfp == 0) { lfp = sp; } cfp = th->cfp = th->cfp - 1; cfp->pc = pc; cfp->sp = sp + 1; cfp->bp = sp + 1; cfp->iseq = iseq; cfp->flag = type; cfp->self = self; cfp->lfp = lfp; cfp->dfp = dfp; cfp->proc = 0;
例のmontecarlo.rbのiseqをフレームを積んだ後のスタックは以下のような感じになります。
|Qnil |GC_GUARDED_PTR(0) |Qnil |GC_GUARDED_PTR(GC_GUARDED_PTR(0)) |Qnil # for n |Qnil # for pi |Qnil # for svar dfp,lfp→|GC_GUARDED_PTR(0) sp,bp→| | ... | cfp→|今積んだフレーム情報 |rb_vm_set_finish_env関数で積んだフレーム情報 |th_init2関数で積んだフレーム情報
この関数はvm_eval関数を実行します。例外やbreakなどが起こるとこの関数に戻ってきて適切な再開アドレスを計算し、再びvm_eval関数を呼び出しています。
この関数がYARV命令実行の肝です。いい感じに難解なコードになっています。YARV命令はコンパイルオプションにより以下のいずれかの形式で実行されます。
詳しいからくりはvm.hを眺めてください。
各命令の処理ルーチンはどこに書かれているかですがvm.incに書かれています。
各命令の動きが知りたい場合はinsns.defを見ると書いてあります。見るとわかりますがinsns.defに書かれているのはCではありません。tool/insns2vm.rbを実行することで各種incファイルが生成されるようです。各命令は以下のフォーマットになっているようです。
DEFINE_INSN 命令名 (引数...) (スタックから拾う値) (スタックに積む値) { Cのソース }
引数の取得やスタックからのポップ、スタックへのプッシュは定義から自動生成されるので命令を書く際に気にする必要はありません。ただし、任意個の要素をスタックからポップ、スタックへプッシュする際は自分で書く必要があります。
YARVでは以下の6つのレジスタが使われています。
命令を記述する際に利用するマクロはinsnhelper.hに書かれています。スタック操作、ローカル変数操作、レジスタ操作などのマクロが定義されています。
命令を記述するする際に利用する関数はvm_insnhelper.cに書かれています。staticですがvm.cがincludeしているので命令内では問題なく使用することができます(vm_eval関数が書かれているvm_evalbody.cもvm.cがincludeしています)。
それではYARVコードへのコンパイルでコンパイルしたコードを実行してみます。
この命令は名が示すようにクラスを定義します。まあそれ自体は珍しくないので珍しい部分を取り上げます。この命令ではスタックからベースクラス(スーパークラスではありません。Outer::Innerとなってる場合のOuterのことです)とスーパークラスを取得します。このうちベースクラスは以下の場合nilです。
class C end
class Outer class Inner end end
ベースクラスがnilの場合、vm_get_cbase関数が呼ばれてベースクラスを取得しています。 まず、フレームをサーチ(vm_get_ruby_level_cfp関数(eval_intern.h))してRubyで書かれたメソッドを実行しているフレームを取得しています。命令の処理ルーチンから呼ばれるので現在のフレームになると思います。 次にフレームのrb_iseq_t構造体からcref_stackという情報を取り出しています(get_cref関数(vm_insnhelper.c))。 その後、cref_stackからベースクラスを取得しています。
というのが処理の流れなのですがcref_stackっていつ設定されたのかというとYARVコードへのコンパイルの際(set_relation関数(iseq.c))に設定されています。
if (type == ISEQ_TYPE_TOP) { /* toplevel is private */ iseq->cref_stack = NEW_BLOCK(th->top_wrapper ? th->top_wrapper : rb_cObject); iseq->cref_stack->nd_file = 0; iseq->cref_stack->nd_visi = NOEX_PRIVATE; } else if (type == ISEQ_TYPE_METHOD || type == ISEQ_TYPE_CLASS) { iseq->cref_stack = NEW_BLOCK(0); /* place holder */ iseq->cref_stack->nd_file = 0; }
というわけでトップレベルのベースクラスは普通Objectです(ベースクラスがObjectの場合はObject::CではなくCという名前になります)。 一方、クラス定義をコンパイルするときは0です。それじゃあ困るんじゃないか?といった疑問を解決するために次に行きます。
クラスを定義するとクラスを表すVALUEが決定されます。先ほどcref_stackのnd_clssが0だったのはこのクラスを表すVALUEが決定できないためです。この関数を呼ぶことでcref_stackにクラスが設定され、Innerをdefineclassする際にベースクラスとしてOuterを取得することができます。
この命令の組はVALUEをキャッシュに設定、キャッシュから取得するためのもので定数の値をキャッシュするなどに使われています。主な目的はクラスを示すVALUEをキャッシュして毎回テーブルを引かなくて済むようにすることだと思います。コンパイル時にOPT_INLINE_CONST_CACHEが有効の場合、定数取得のために出力されるYARVコードは以下のようになります。
:label(5) getinlinecache(0, label(6)) getconstant(:MonteCarlo) setinlinecache(label(5)) :label(6)
上の疑似コードはNODEがどういうYARV命令に変換されるかを示したものなのでgetinlinecacheの第1引数が0になってますが、VALUEコードとして設定される際にちゃんと領域が確保されます。
compile.c
VALUE v = (VALUE)NEW_INLINE_CACHE_ENTRY(); generated_iseq[pos + 1 + j] = v;
vm_core.h
#define NEW_INLINE_CACHE_ENTRY() NEW_WHILE(Qundef, 0, 0) #define ic_class u1.value #define ic_method u2.node #define ic_value u2.value #define ic_vmstat u3.cnt typedef NODE *IC;
getinlinecacheの処理ルーチンは以下のようになっています。
DEFINE_INSN getinlinecache (IC ic, OFFSET dst) () (VALUE val) { if (ic->ic_vmstat == GET_VM_STATE_VERSION()) { val = ic->ic_value; JUMP(dst); } else { /* none */ val = Qnil; } }
次にsetinlinecacheの処理ルーチンです。
DEFINE_INSN setinlinecache (OFFSET dst) (VALUE val) (VALUE val) { IC ic = GET_CONST_INLINE_CACHE(dst); ic->ic_value = val; ic->ic_vmstat = GET_VM_STATE_VERSION(); }
insnhelper.h
#define GET_CONST_INLINE_CACHE(dst) ((IC) * (GET_PC() + (dst) + 1)) 若干わかりにくいので解説しておくと、getinlinecacheの第1引数を取得しています
定数取得の場合、setinlinecacheの前にgetconstantがあるのでスタックトップには定数名に対応したVALUEが積まれています。それを拾ってキャッシュに保存後、またスタックに積んでおくということを行っています。
後はGET_VM_STATE_VERSIONマクロが何者かわかれば理解できそうです。
vm.h
#define GET_VM_STATE_VERSION() (ruby_vm_global_state_version) #define INC_VM_STATE_VERSION() \ (ruby_vm_global_state_version = (ruby_vm_global_state_version+1) & 0x8fffffff)
vm.c
rb_vm_change_state(void) { INC_VM_STATE_VERSION(); }
variable.c
mod_av_set(VALUE klass, ID id, VALUE val, int isconst) { ... if(isconst){ rb_vm_change_state(); } rb_const_set(VALUE klass, ID id, VALUE val) { ... mod_av_set(klass, id, val, Qtrue); }
というわけで、定数が設定されるとバージョンが上がっています。ご存じのようにRubyでは定数と言っておきながら何回でも代入できるので代入されたらキャッシュは無効になるというからくりのようです*1。
この命令はメソッドを呼び出します。おそらくもっともよく実行される命令でしょう。
レシーバはFCALL_BITが設定されているとself、それ以外はTOPN(num)です。TOPN(num)が何故レシーバかというとスタックにはこう積まれているからです。
TOPN(2)→|レシーバ TOPN(1)→|引数1 TOPN(0)→|引数2 sp→|
その後、CALL_METHODマクロ経由でvm_call_method関数が呼び出され、メソッドの種類(Rubyで書かれてるかCで書かれているかなど)に応じて処理が行われています。Rubyで書かれたメソッドの場合、vm_setup_method関数に処理が移っています。
vm_setup_method関数ではまずvm_callee_setup_arg関数を呼び出してデフォルト引数や可変長引数の処理を行っています。いろいろ興味深いのですが例題の都合で実際の値でトレースすることができないので深追いは止めます。
その後、leaveの前のsendかによって分岐してますが、vm_push_frame関数を呼んでメソッド呼び出し完了です。・・・ちょっとだまされたような感じです。何でメソッド呼び出し完了なのかというと
sendを呼び出す前はこうです。
reg_cfp->pc→send(:pi, 1, 0, 0, 0) setlocal(1) # pi
sendを呼び出した後はこうです。
reg_cfp->pc→putobject(0)
呼び出したとは書きましたが返ってきたとは書いてません:-)
メソッドにブロックを渡す場合を考えましょう。まずcaller_setup_arguments関数にてrb_block_t構造体が設定されています。
vm_insnhelper.c
caller_setup_args(rb_thread_t *th, rb_control_frame_t *cfp, VALUE flag, int argc, rb_iseq_t *blockiseq, rb_block_t **block) { rb_block_t *blockptr = 0; if (block) { if (flag & VM_CALL_ARGS_BLOCKARG_BIT) { ... else if (blockiseq) { blockptr = RUBY_VM_GET_BLOCK_PTR_IN_CFP(cfp); blockptr->iseq = blockiseq; blockptr->proc = 0; *block = blockptr; }
RUBY_VM_GET_BLOCK_PTR_IN_CFPマクロはvm_core.hに書かれていますが若干不可解です。
#define RUBY_VM_GET_BLOCK_PTR_IN_CFP(cfp) ((rb_block_t *)(&(cfp)->self))
何故これでrb_control_frame_tからrb_block_tが取れるかというとこういうことです。
rb_control_t | rb_block_t | |
VALUE* | pc | |
VALUE* | sp | |
VALUE* | bp | |
rb_iseq_t* | iseq | |
VALUE | flag | |
VALUE | self | self |
VALUE* | lfp | lfp |
VALUE* | dfp | dfp |
rb_iseq_t* | block_iseq | iseq |
VALUE | proc | proc |
ID | method_id | |
VALUE | method_class | |
VALUE | prof_time_self | |
VALUE | prof_time_chld |
つまり、selfから5個の型が同じなのでキャストによりrb_block_tになるわけです。
次にブロックが呼び出される部分を見てみましょう。Cでメソッドを書く場合rb_yield関数(eval.c)を呼び出します。その後、rb_yield_0関数経由でvm_yield関数が呼ばれます。
vm_yield関数ではlfpが指すアドレスからrb_block_t構造体を取得しています。念のためメソッド呼び出しの部分を見てみると、
vm_setup_method(rb_thread_t *th, rb_control_frame_t *cfp, int argc, rb_block_t *blockptr, VALUE flag, VALUE iseqval, VALUE recv, VALUE klass) { ... vm_push_frame(th, iseq, FRAME_MAGIC_METHOD, recv, (VALUE) blockptr, iseq->iseq_encoded + opt_pc, sp, 0, 0);
となっています。その後、invoke_block関数にてフレームを積んでいます。
vm_push_frame(th, iseq, type, self, GC_GUARDED_PTR(block->dfp), iseq->iseq_encoded + opt_pc, cfp->sp + arg_size, block->lfp, iseq->local_size - arg_size);
スタックがどうなるか見てみましょう。サンプルは例によってmontecarlo.rbです。
|Qnil # for n |Qnil # for count |Qnil # for svar lfp→|GC_GUARDED_PTR(0) |Qnil # for x |Qnil # for y dfp→|GC_GUARDED_PTR(block->dfp) sp,bp→|
今まで見てきたのと異なり、lfpとdfpが同じではありません。
今回はYARVコードへの実行を見てきました。YARVコードまで落ちると個々の命令は単純なので実行モデルが理解できれば他の命令も理解できると思います。
一応ここまででrubyコマンドを打ってからスクリプトが実行されるまでの流れは終了です。ただしたっぷり未解決のものがあるのでそれらは個別に書きたいと思います。それではみなさんもよいコードリーディングを。