自分向けの備忘録として、MRIのソースコードを読んだ結果メモを記録しておきます。
以下のソースコードは
def m 1 end
次のISeqにコンパイルされます。
== disasm: <RubyVM::InstructionSequence:<main>@-e>====================== 0000 trace 1 ( 1) 0002 putspecialobject 1 0004 putspecialobject 2 0006 putobject :m 0008 putiseq m 0010 opt_send_simple <callinfo!mid:core#define_method, argc:3, ARGS_SKIP> 0012 leave
putspecialobjectはその名の通り特殊なオブジェクトをスタックにプッシュします。引数で種類を指定します。種類はそれぞれ
値 | 名称 | オブジェクト |
---|---|---|
1 | VM_SPECIAL_OBJECT_VMCORE | VMを表す内部オブジェクト |
2 | VM_SPECIAL_OBJECT_CBASE | メソッドが定義されるクラス |
3 | VM_SPECIAL_OBJECT_CONST_BASE | 定数検索ののベースとなるクラス |
です。insn.defのputspecialobjectの定義参照。
メソッド定義で使うのは1と2です。1はVMを表すオブジェクトですが、このオブジェクトはvm.c:Init_VM()で定義されており、グローバル変数rb_mRubyVMFrozenCoreに入っています。グローバル変数をわざわざ引数として渡しているのは将来のマルチVM化の複線かもしれません。
2はメソッドが定義されるべきクラスで、vm_insnhelper.c:vm_get_cbase()で取得されています。
static inline VALUE vm_get_cbase(const rb_iseq_t *iseq, const VALUE *ep) { NODE *cref = rb_vm_get_cref(iseq, ep); VALUE klass = Qundef; while (cref) { if ((klass = cref->nd_clss) != 0) { break; } cref = cref->nd_next; } return klass; }
この関数では、まずvm_insnhelper.c:rb_vm_get_cref()を呼んでcrefなるものを取得しています。これは、「Rubyソースコード徹底解説」によればクラスのネスト関係を表すリストだそうです。VALUE型の変数ですが、nd_clssがクラスを表し、nd_nextで外側のクラスを表します。どうやらnd_clssは0になることがあるようで、関数の後半部分ではnd_clssが0の間、外側のクラスを辿っています。
vm_insnhelper.c:rb_vm_get_cref()ですが、ソースコードは以下の通りです。
NODE * rb_vm_get_cref(const rb_iseq_t *iseq, const VALUE *ep) { NODE *cref = vm_get_cref0(iseq, ep); if (cref == 0) { rb_bug("rb_vm_get_cref: unreachable"); } return cref; }
さらにvm_insnhelper.c:vm_get_cref0()は以下の通りです。
static NODE * vm_get_cref0(const rb_iseq_t *iseq, const VALUE *ep) { while (1) { if (VM_EP_LEP_P(ep)) { if (!RUBY_VM_NORMAL_ISEQ_P(iseq)) return NULL; return iseq->cref_stack; } else if (ep[-1] != Qnil) { return (NODE *)ep[-1]; } ep = VM_EP_PREV_EP(ep); } }
ここでiseqとepという変数が使われていますが、iseqはコンパイルされたメソッドの実体で、そこにメソッドが定義された場所のcrefが保管されているようです。rb_iseq_tの定義はvm_core.hにあります。
epはValue Stack(YARVでは2本のスタックが使われており、そのうちの1本です)上のメソッドのローカルフレームを指すポインタです。x86ではebp/rbpで管理されるローカルフレームに相当します。どうやらそのep[-1]にもcrefが保管されているようです。
どちらのcrefを使うかはVM_EP_LEP_P()なるマクロで判別します。この定義はvm_core.hにあり、
/* * block frame: * ep[ 0]: prev frame * ep[-1]: CREF (for *_eval) * * method frame: * ep[ 0]: block pointer (ptr | VM_ENVVAL_BLOCK_PTR_FLAG) */ #define VM_ENVVAL_BLOCK_PTR_FLAG 0x02 #define VM_ENVVAL_BLOCK_PTR_P(v) ((v) & VM_ENVVAL_BLOCK_PTR_FLAG) #define VM_EP_LEP_P(ep) VM_ENVVAL_BLOCK_PTR_P((ep)[0])
となっています。ep[0]に入っているのがブロックポインタかどうか判定することで、今いる場所がメソッドかどうかを判定しているようです。メソッドであればiseqに保管されたcrefを使い、ブロックであれば、ブロックのコンパイル時に構築されるiseqにcrefは定義されないので、フレームに積まれたcrefを使うということになります。
これで最初の2つの引数が何か明らかになりました。それ以外の引数は、それぞれメソッド名とメソッドの実体です。これらの引数でcore#define_methodを読んでいます。
長くなったのでcore#define_methodは次の記事で。