msumimz's diary

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

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

さて、(上)に続いてメソッド定義の実装を見ていきます。

core#define_methodの実体はvm.c:m_core_define_method()です。

#define REWIND_CFP(expr) do { \
    rb_thread_t *th__ = GET_THREAD(); \
    th__->cfp++; expr; th__->cfp--; \
} while (0)

static VALUE
m_core_define_method(VALUE self, VALUE cbase, VALUE sym, VALUE iseqval)
{
    REWIND_CFP({
	vm_define_method(GET_THREAD(), cbase, SYM2ID(sym), iseqval, 0, rb_vm_cref());
    });
    return sym;
}

REWIND_CFPは、コントロールフレームを一時的に一段巻き戻した上で処理を行うマクロです。core#define_method自身のコントロールフレームが積まれていますので、それを巻き戻すということでしょう。

vm.c:rb_vm_cref()はその名の通りcrefを求める関数です。定義は以下の通りです。

NODE *
rb_vm_cref(void)
{
    rb_thread_t *th = GET_THREAD();
    rb_control_frame_t *cfp = rb_vm_get_ruby_level_next_cfp(th, th->cfp);

    if (cfp == 0) {
	return NULL;
    }
    return rb_vm_get_cref(cfp->iseq, cfp->ep);
}

先ほど巻き戻したコントロールフレームですが、vm.c:rb_vm_get_ruby_level_next_cfp()を呼んでさらに巻き戻しています。メソッド名からするとスクリプトレベルのコントロールフレームに巻き戻しているようですが、内容がよくわかりませんでした。そのようにして求めたコントロールフレームに対して、前回記事で説明したrb_vm_get_cref()を呼んでいます。

今回のサンプルコードでvm.c:rb_vm_cref()を実行した結果は、前回、cbaseを求める過程で求めたcrefと同じ値でした。きちんと調べるのであれば、2つのcrefが異なるときにブレークするような条件文を挿入して適当なスクリプトを実行してみればよさそうですが、今回は省略します。

さて、vm.c:vm_define_method()の実装は以下の通りです。

static void
vm_define_method(rb_thread_t *th, VALUE obj, ID id, VALUE iseqval,
		 rb_num_t is_singleton, NODE *cref)
{
    VALUE klass = cref->nd_clss;
    int noex = (int)cref->nd_visi; // (A)
    rb_iseq_t *miseq;
    GetISeqPtr(iseqval, miseq); // (B)

    if (miseq->klass) { // (C)
	RB_GC_GUARD(iseqval) = rb_iseq_clone(iseqval, 0);
	GetISeqPtr(iseqval, miseq);
    }

    if (NIL_P(klass)) {
	rb_raise(rb_eTypeError, "no class/module to add method");
    }

    if (is_singleton) {
	klass = rb_singleton_class(obj); /* class and frozen checked in this API */
	noex = NOEX_PUBLIC;
    }

    /* dup */
    COPY_CREF(miseq->cref_stack, cref); // (D-1)
    miseq->cref_stack->nd_visi = NOEX_PUBLIC; // (D-2)
    RB_OBJ_WRITE(miseq->self, &miseq->klass, klass);
    miseq->defined_method_id = id;
    rb_add_method(klass, id, VM_METHOD_TYPE_ISEQ, miseq, noex); // (E)

    if (!is_singleton && noex == NOEX_MODFUNC) { // (F)
	klass = rb_singleton_class(klass);
	rb_add_method(klass, id, VM_METHOD_TYPE_ISEQ, miseq, NOEX_PUBLIC);
    }
}

いろいろやっていますが、基本的には(E)でメソッドを定義しているだけです。

(A)で、既定のvisibilityを取得しています。

(B)のGetISeqPtrはMRIでよくある種類のマクロで、GetISeqPtr(a, b)で、単にb = (rb_seq_t*)aを表します。このマクロを定義している理由は、デバッグ時に型チェックを行う追加処理を入れるためのようです(vm_core.hで定義)。

(C)は、miseq->klassが定義済みとは、他のメソッドで使われているISeqという意味でしょう。ISeqを別のメソッド定義で再利用する場合は、rb_iseq_clone()でコピーしてから使っています(共用できないのは、インラインメソッドキャッシュのためでしょうか)。

(D-1)、(D-2)でcrefをコピーしたうえでvisibilityをNOEX_PUBLICにしているのは、例えば以下の場合でしょうか。

class C
  private
  def m1
    def m2
      1
    end
  end
end

C.new.m1        # => NoMethodError: private method `m1' called for C
C.new.send(:m1) # => クラスCにpublicなメソッドm2が定義される
C.new.m2        # => 1

クラススコープで設定された既定のvisibilityはメソッド内部には伝播せず、メソッド内部でメソッドを定義すると常にpublicになります。

(F)は、module_functionの処理でしょう。クラスの特異クラスに同じメソッドを定義しています。こちらはISeqを共用していますね。

(E)のrb_add_method()については次回。