msumimz's diary

RubyにJITコンパイラを実装する個人プロジェクトの情報発信ブログです。

MRIソースコードを読む その1 メソッド定義の実装(上)

自分向けの備忘録として、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は次の記事で。