msumimz's diary

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

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

vm_method.c:rb_add_method()ですが、エラーチェックしながらデータを設定しているだけです。せっかくですのでJITコンパイラを実装するために必要そうな知識を整理しておきます。

メソッド定義に関係するデータ構造は、rb_method_entry_tとrb_method_definition_tで、method.hで定義されています。

まずrb_method_entry_tです。一見複雑そうですが、メソッドの種類ごとの本体データが共用体になっているだけです。

typedef struct rb_method_definition_struct {
    rb_method_type_t type; /* method type */
    ID original_id;
    union {
	rb_iseq_t * const iseq;            /* should be mark */
	rb_method_cfunc_t cfunc;
	rb_method_attr_t attr;
	const VALUE proc;                 /* should be mark */
	enum method_optimized_type {
	    OPTIMIZED_METHOD_TYPE_SEND,
	    OPTIMIZED_METHOD_TYPE_CALL,

	    OPTIMIZED_METHOD_TYPE__MAX
	} optimize_type;
	struct rb_method_entry_struct *orig_me;
    } body;
    int alias_count;
} rb_method_definition_t;

typeメンバでメソッドの種類を表します。MRIの内部では、メソッドは以下の11種類に分類されます。

typedef enum {
    VM_METHOD_TYPE_ISEQ,
    VM_METHOD_TYPE_CFUNC,
    VM_METHOD_TYPE_ATTRSET,
    VM_METHOD_TYPE_IVAR,
    VM_METHOD_TYPE_BMETHOD,
    VM_METHOD_TYPE_ZSUPER,
    VM_METHOD_TYPE_UNDEF,
    VM_METHOD_TYPE_NOTIMPLEMENTED,
    VM_METHOD_TYPE_OPTIMIZED, /* Kernel#send, Proc#call, etc */
    VM_METHOD_TYPE_MISSING,   /* wrapper for method_missing(id) */
    VM_METHOD_TYPE_REFINED,

    END_OF_ENUMERATION(VM_METHOD_TYPE)
} rb_method_type_t;

いずれすべての種類を理解しなければいけませんが、当面必要なのはVM_METHOD_TYPE_ISEQとVM_METHOD_TYPE_CFUNCの2つです。VM_METHOD_TYPE_ISEQはスクリプトで定義されたメソッドで、メソッド本体はiseqです。

VM_METHOD_TYPE_CFUNCはCで実装されたメソッドで、メソッドの実体はrb_method_func_t型のcfuncです(method.hで定義)。

typedef struct rb_method_cfunc_struct {
    VALUE (*func)(ANYARGS);
    VALUE (*invoker)(VALUE (*func)(ANYARGS), VALUE recv, int argc, const VALUE *argv);
    int argc;
} rb_method_cfunc_t;

invokerが少し面白いメンバで、vm_method.c:rb_add_method()からvm_method.c:setup_method_cfunct_strct()経由で呼び出されるvm_method.c:call_cfunc_invoker_func()で設定されます。実装は以下の通りです。

static VALUE
(*call_cfunc_invoker_func(int argc))(VALUE (*func)(ANYARGS), VALUE recv, int argc, const VALUE *)
{
    switch (argc) {
      case -2: return &call_cfunc_m2;
      case -1: return &call_cfunc_m1;
      case 0: return &call_cfunc_0;
      case 1: return &call_cfunc_1;
      case 2: return &call_cfunc_2;
      case 3: return &call_cfunc_3;
      case 4: return &call_cfunc_4;
      case 5: return &call_cfunc_5;
      case 6: return &call_cfunc_6;
      case 7: return &call_cfunc_7;
      case 8: return &call_cfunc_8;
      case 9: return &call_cfunc_9;
      case 10: return &call_cfunc_10;
      case 11: return &call_cfunc_11;
      case 12: return &call_cfunc_12;
      case 13: return &call_cfunc_13;
      case 14: return &call_cfunc_14;
      case 15: return &call_cfunc_15;
      default:
	rb_bug("call_cfunc_func: unsupported length: %d", argc);
    }
}

つまり、メソッドの引数ごとに呼び出すヘルパメソッドを設定しています。例えばcall_cfunc_3を見てみると(vm_insnhelper.cで定義)、以下のような実装です。

static VALUE
call_cfunc_3(VALUE (*func)(ANYARGS), VALUE recv, int argc, const VALUE *argv)
{
    return (*func)(recv, argv[0], argv[1], argv[2]);
}

そのままですね。

次にrb_method_entry_tですが、定義は以下の通りです。

typedef struct rb_method_entry_struct {
    rb_method_flag_t flag;
    char mark;
    rb_method_definition_t *def;
    ID called_id;
    VALUE klass;                    /* should be mark */
} rb_method_entry_t;

このrb_method_entry_tが、クラスのメソッドテーブル(mtbl)というハッシュに定義されます。flagはメソッドのvisibilityなどの状態を保持するビット列です。called_idは呼び出されるメソッド名で、エイリアスによって別名定義された場合に元の名前とは異なる名前になります。rb_method_definition_tにあるoriginal_idが元の名前です。