mrubyを読む

はじめに

コードも生成できたので最後にコードを実行している部分を読みます。mrb_run()がエントリポイントになります。

mrubyVM概観

いきなりmrb_run()に入る前にmrubyVM*1がどんな実行モデルなのかについて説明します。

mrubyVMの実行モデルはレジスタマシンです。ちなみにYARVはスタックマシンです。レジスタマシンとスタックマシンの違いはWikipediaあたりをご参照ください。

例えば以下の単純なRubyスクリプトの場合、

def foo(a, b)
  a * b
end

f = foo(1, 2)
irep 116 nregs=6 nlocals=3 pools=0 syms=1
000 OP_TCLASS   R3
001 OP_LAMBDA   R4      I(117)  1
002 OP_METHOD   R3      'foo'
003 OP_LOADSELF R3
004 OP_LOADI    R4      1
005 OP_LOADI    R5      2
006 OP_LOADNIL  R6
007 OP_SEND     R3      'foo'   2
008 OP_MOVE     R1      R3
009 OP_STOP

irep 117 nregs=7 nlocals=5 pools=0 syms=1
000 OP_ENTER    2:0:0:0:0:0:0
001 OP_MOVE     R5      R1
002 OP_MOVE     R6      R2
003 OP_LOADNIL  R7
004 OP_SEND     R5      '*'     1
005 OP_RETURN   R5

f = foo(1, 2)の部分は以下のように実行されます。

  1. レジスタR3にselfをロード(レシーバを設定)
  2. レジスタR4に1をロード(引数を設定)
  3. レジスタR5に2をロード(引数を設定)
  4. レジスタR6にnilをロード(ブロック引数を設定)
  5. レジスタR3のオブジェクトに対して'foo'メソッドを引数が2つで実行
  6. レジスタR1(ローカル変数f)にメソッド呼び出しの結果(R3に格納されます)を設定

mrubyではレジスタの確保場所としてスタックを使用しています。そのため、mrubyVMはスタックマシンであると勘違いしてしまう危険があるので注意してください。さわだもソースだけ見ていてスタックマシンだと勘違いしていました。

レジスタの確保場所としてスタックを使うとはどういうことかというと、以下のようなイメージです。(OP_SEND '*'実行直前の状態)

トップレベル実行時のスタックベース→| nil |top_self
                                    |     |ローカル変数fの格納領域
                                    | nil |よくわからない。特殊変数用?
 メソッドfoo実行時のスタックベース→| nil |'foo'のレシーバ
                                    |  1  |'foo'の引数1 & ローカル変数a
                                    |  2  |'foo'の引数2 & ローカル変数b
                                    | nil |'foo'に対するブロック引数
                                    | nil |よくわからない。特殊変数用?
                                    |  1  |'*'のレシーバ
                                    |  2  |'*'の引数1
                                    | nil |'*'に対するブロック引数

以上の前提を持ってmrb_run()に挑むと理解が深まると思います。

mrb_run(src/vm.c)

では、mrb_run()に見ていくことにしましょう。mrb_run()は一言で言うと一つ一つ命令を実行するループと各命令の処理に振り分ける巨大なswitch文です。ただし、処理効率化のためにちょっとカラクリが施されています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
-
|
|
!
|
-
|
|
|
|
|
|
|
!
|
|
!
 
  INIT_DISPACTH {
    CASE(OP_NOP) {
      /* do nothing */
      NEXT;
    }
 
    CASE(OP_MOVE) {
      /* A B    R(A) := R(B) */
      int a = GETARG_A(i);
      int b = GETARG_B(i);
 
      regs[a].tt = regs[b].tt;
      regs[a].value = regs[b].value;
      NEXT;
    }
 
    ...
  }
  END_DISPACTH;

INIT_DISPACTH*2*3, CASE, NEXTの定義はmrb_run()の少し上に書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 
 
 
 
 
-
|
|
|
!
 
 
 
 
 
 
 
 
 
 
 #ifdef __GNUC__
 #define DIRECT_THREADED
 #endif
 
 #ifndef DIRECT_THREADED
 
 #define INIT_DISPACTH for (;;) { i = *pc; switch (GET_OPCODE(i)) {
 #define CASE(op) case op:
 #define NEXT mrb->arena_idx = ai; pc++; break
 #define JUMP break
 #define END_DISPACTH } }
 
 #else
 
 #define INIT_DISPACTH JUMP; return mrb_nil_value();
 #define CASE(op) L_ ## op:
 #define NEXT mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)]
 #define JUMP i=*pc; goto *optable[GET_OPCODE(i)]
 #define END_DISPACTH
 
 #endif

gccかどうかで定義が変ってます。めんどくさいけどちゃんとマクロ展開されたコードを示すことにします。

gccじゃない場合

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
-
|
|
!
|
-
|
|
|
|
|
|
|
!
|
|
!
!
  for (;;) { i = *pc; switch (GET_OPCODE(i)) { {
    case OP_NOP: {
      /* do nothing */
      mrb->arena_idx = ai; pc++; break;
    }
 
    case OP_MOVE: {
      /* A B    R(A) := R(B) */
      int a = GETARG_A(i);
      int b = GETARG_B(i);
 
      regs[a].tt = regs[b].tt;
      regs[a].value = regs[b].value;
      mrb->arena_idx = ai; pc++; break;
    }
 
    ...
  }
  } };

gccの場合

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
-
|
|
!
|
-
|
|
|
|
|
|
|
!
|
|
!
 
  i=*pc; goto *optable[GET_OPCODE(i)]; return mrb_nil_value(); {
    L_OP_NOP: {
      /* do nothing */
      mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)];
    }
 
    L_OP_MOVE: {
      /* A B    R(A) := R(B) */
      int a = GETARG_A(i);
      int b = GETARG_B(i);
 
      regs[a].tt = regs[b].tt;
      regs[a].value = regs[b].value;
      mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)];
    }
 
    ...
  }
  ;

というわけでgccじゃない場合は無限ループ & switch文ですが、gccの場合は命令コードで決まるジャンプ先に直接飛んでいます。こうすることで命令ごとに条件分岐をするコストがなくなるため高速化が実現できます。YARVでも同じことが行われていました。

実行してみる

総論は終わったので後は各論、いつものように各命令についてどのような処理が行われているかを見ていくことにします。なお、例外処理関係は別で扱う予定なので飛ばします。

OP_LOADSELF

mrb_run()を上から見ていくと初めはLOAD系の命令が並んでいます。その中でややわかりにくいのがLOADSELFだと思います。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
-
|
|
|
!
    CASE(OP_LOADSELF) {
      /* A      R(A) := self */
      regs[GETARG_A(i)] = mrb->stack[0];
      NEXT;
    }

何でこれでselfを設定したことになるのか?というと、スタックベースにはselfのオブジェクトが格納されているからです。上に書いたスタックのイメージを再掲すると、

メソッドfoo実行時のスタックベース→| nil |'foo'のレシーバ
                                   |  1  |'foo'の引数1 & ローカル変数a
                                   |  2  |'foo'の引数2 & ローカル変数b
                                   | nil |'foo'に対するブロック引数

というわけでstack[0]がselfになっていることがご理解いただけると思います。

このstack[0]がselfであるということは他でも使われているので覚えておくようにしてください。例えば以下のようにインスタンス変数を取得するコードでもしれっと使われています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
-
|
|
|
!
    CASE(OP_GETIV) {
      /* A Bx   R(A) := ivget(Bx) */
      regs[GETARG_A(i)] = mrb_vm_iv_get(mrb, syms[GETARG_Bx(i)]);
      NEXT;
    }

src/variable.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
-
|
|
!
mrb_value
mrb_vm_iv_get(mrb_state *mrb, mrb_sym sym)
{
  /* get self */
  return mrb_iv_get(mrb, mrb->stack[0], sym);
}

OP_SEND

次にメソッド呼び出しを行うOP_SENDを見てみましょう。長いので例によって区切って解説します。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
|
|
|
|
|
|
|
|
    CASE(OP_SEND) {
      /* A B C  R(A) := call(R(A),Sym(B),R(A+1),... ,R(A+C-1)) */
      int a = GETARG_A(i);
      int n = GETARG_C(i);
      struct RProc *m;
      struct RClass *c;
      mrb_callinfo *ci;
      mrb_value recv;
      mrb_sym mid = syms[GETARG_B(i)];

コメントに書いてあるように、OP_SENDは3引数型の命令でそれぞれ以下の内容になっています。

第1引数
メソッドのレシーバが格納されているレジスタ
第2引数
メソッドのシンボルテーブルへのインデックス番号
第3引数
メソッドに渡す引数の数。引数はレシーバが格納されているレジスタの次のレジスタからn個格納する

と、これがmrubyのメソッド呼び出し規約となっているようです。スタックイメージを再々掲すると、

| nil |R(A)   'foo'のレシーバ
|  1  |R(A+1) 'foo'の引数1
|  2  |R(A+2) 'foo'の引数2

となっており、適切に呼び出す準備がされていることがわかります。*4

先に進みます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 
 
 
-
|
|
|
|
-
|
!
-
|
|
|
!
!
      recv = regs[a];
      c = mrb_class(mrb, recv);
      m = mrb_method_search_vm(mrb, &c, mid);
      if (!m) {
        mrb_value sym = mrb_symbol_value(mid);
 
        mid = mrb_intern(mrb, "method_missing");
        m = mrb_method_search_vm(mrb, &c, mid);
        if (n == CALL_MAXARGS) {
          mrb_ary_unshift(mrb, regs[a+1], sym);
        }
        else {
          memmove(regs+a+2, regs+a+1, sizeof(mrb_value)*(n+1));
          regs[a+1] = sym;
          n++;
        }
      }

呼び出すメソッドの検索をしています。メソッドがない場合はmethod_missingを呼ぶようにしています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 
 
 
 
 
 
 
 
 
      /* push callinfo */
      ci = cipush(mrb);
      ci->mid = mid;
      ci->proc = m;
      ci->stackidx = mrb->stack - mrb->stbase;
      ci->argc = n;
      if (ci->argc == CALL_MAXARGS) ci->argc = -1;
      ci->target_class = m->target_class;
      ci->pc = pc + 1;

呼び出し情報はmrb_callinfo構造体に格納されるようです。ここで覚えておくといいのはstackidxとpcがメソッドから返ってきたときの復元に使われる情報ということでしょうか。

Everything is expanded.Everything is shortened.
  1
  2
 
 
      /* prepare stack */
      mrb->stack += a;

スタックベースを調整してレシーバが入っているスタック位置をstack[0]、すなわちselfになるようにしています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
|
|
|
|
|
|
|
!
      if (MRB_PROC_CFUNC_P(m)) {
        mrb->stack[0] = m->body.func(mrb, recv);
        mrb->arena_idx = ai;
        if (mrb->exc) goto L_RAISE;
        /* pop stackpos */
        mrb->stack = mrb->stbase + ci->stackidx;
        cipop(mrb);
        NEXT;
      }

メソッドがCで書かれている場合です。Cで書かれたメソッドの処理はまた別に見ます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
-
|
|
|
|
|
|
|
|
|
-
|
!
-
|
!
|
|
|
!
!
      else {
        /* fill callinfo */
        ci->acc = a;
 
        /* setup environment for calling method */
        proc = mrb->ci->proc = m;
        irep = m->body.irep;
        pool = irep->pool;
        syms = irep->syms;
        ci->nregs = irep->nregs;
        if (ci->argc < 0) {
          stack_extend(mrb, (irep->nregs < 3) ? 3 : irep->nregs, 3);
        }
        else {
          stack_extend(mrb, irep->nregs,  ci->argc+2);
        }
        regs = mrb->stack;
        pc = irep->iseq;
        JUMP;
      }
    }

メソッドがRubyで書かれている場合はこちらが実行されます。mrb_callinfo.accは先に書いてしまうとメソッドの戻り値をどこに入れるかの情報です。第1引数が保存されているのでレシーバを設定したレジスタに戻り値が格納されることがわかります。その後、各種実行時情報を呼び出し先のものに切り替えています。注意が必要なのはここでは呼び出すだけでメソッドから返ってくるのはOP_RETURNが実行されたときであるということです。

OP_ENTER

メソッド呼び出しについて見たので次はメソッドの受け側であるOP_ENTERを見ます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
-
|
|
|
|
|
|
|
-
|
|
|
!
    CASE(OP_ENTER) {
      /* Ax             arg setup according to flags (24=5:5:1:5:5:1:1) */
      /* number of optional arguments times OP_JMP should follow */
      int ax = GETARG_Ax(i);
      int m1 = (ax>>18)&0x1f;
      int o  = (ax>>13)&0x1f;
      int r  = (ax>>12)&0x1;
      int m2 = (ax>>7)&0x1f;
      /* unused
      int k  = (ax>>2)&0x1f;
      int kd = (ax>>1)&0x1;
      int b  = (ax>>0)& 0x1;
      */

まず、命令コードのオペランドから各種引数の数を取得しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
      int argc = mrb->ci->argc;
      mrb_value *argv = regs+1;
      int len = m1 + o + r + m2;

次に実際に渡された引数の取得です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
-
|
|
|
|
!
      if (argc < 0) {
        struct RArray *ary = mrb_ary_ptr(regs[1]);
        argv = ary->buf;
        argc = ary->len;
        regs[len+2] = regs[1];  /* save argary in register */
      }

引数が配列(foo(*a)みたいなの)で渡されたときの処理です。ん?上記のコードを見るとメソッド呼び出し時に存在する不自然な空きレジスタって引数配列退避用なのか?

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
-
-
-
|
|
!
!
!
      if (mrb->ci->proc && MRB_PROC_STRICT_P(mrb->ci->proc)) {
        if (argc >= 0) {
          if (argc < m1 + m2 || (r == 0 && argc > len)) {
        argnum_error(mrb, m1+m2);
        goto L_RAISE;
          }
        }
      }

引数が少ない、もしくは多過ぎるときのエラー処理です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
-
|
|
!
      else if (len > 1 && argc == 1 && argv[0].tt == MRB_TT_ARRAY) {
        argc = mrb_ary_ptr(argv[0])->len;
        argv = mrb_ary_ptr(argv[0])->buf;
      }

yieldの場合、*を付けなくても配列展開されるようです。上記でやっているのはその処理です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
|
|
-
|
!
|
!
      mrb->ci->argc = len;
      if (argc < len) {
        regs[len+1] = argv[argc]; /* move block */
        memmove(®s[1], argv, sizeof(mrb_value)*(argc-m2)); /* m1 + o */
        memmove(®s[len-m2+1], &argv[argc-m2], sizeof(mrb_value)*m2); /* m2 */
        if (r) {                  /* r */
          regs[m1+o+1] = mrb_ary_new_capa(mrb, 0);
        }
        pc += argc - m1 - m2 + 1;
      }

オプション引数の処理です。引数の数からメソッドの開始位置を決定しています。実行コード生成に載せたサンプルを再掲します。

def foo(a = 1, b = 'xxx')
end
irep 117 nregs=6 nlocals=5 pools=1 syms=0
000 OP_ENTER    0:2:0:0:0:0:0
001 OP_JMP              004
002 OP_JMP              005
003 OP_JMP              006
004 OP_LOADI    R1      1
005 OP_STRING   R2      'xxx'
006 OP_RETURN   R4
Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
|
-
|
!
|
|
|
!
      else {
        memmove(®s[1], argv, sizeof(mrb_value)*(m1+o)); /* m1 + o */
        if (r) {                  /* r */
          regs[m1+o+1] = mrb_ary_new_elts(mrb, argc-m1-o-m2, argv+m1+o);
        }
        memmove(®s[m1+o+r+1], &argv[argc-m2], sizeof(mrb_value)*m2);
        regs[len+1] = argv[argc]; /* move block */
        pc += o + 1;
      }

こっちは引数が足りているときです。余った分は残余引数に詰め込んでいます。

Everything is expanded.Everything is shortened.
  1
  2
 
!
      JUMP;
    }

最後に、渡された引数と想定している引数から計算されたpc*5から実行を開始です。

OP_RETURN

続いて、メソッドからの復帰時に実行されるOP_RETURNを見てみましょう。なお、前半で行われている例外処理は省略します。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
-
|
|
-
|
!
-
|
|
|
    CASE(OP_RETURN) {
      /* A      return R(A) */
    L_RETURN:
      if (mrb->exc) {
        (省略)
      }
      else {
        mrb_callinfo *ci = mrb->ci;
        int acc, eidx = mrb->ci->eidx;
        mrb_value v = regs[GETARG_A(i)];

まず、メソッドの戻り値をレジスタから取り出しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
-
|
-
|
|
!
|
|
|
-
|
|
!
|
|
|
-
|
|
!
|
|
|
|
|
!
        switch (GETARG_B(i)) {
        case OP_R_NORMAL:
          if (ci == mrb->cibase) {
            localjump_error(mrb, "return");
            goto L_RAISE;
          }
          ci = mrb->ci;
          break;
        case OP_R_BREAK:
          if (proc->env->cioff < 0) {
            localjump_error(mrb, "break");
            goto L_RAISE;
          }
          ci = mrb->ci = mrb->cibase + proc->env->cioff + 1;
          break;
        case OP_R_RETURN:
          if (proc->env->cioff < 0) {
            localjump_error(mrb, "return");
            goto L_RAISE;
          }
          ci = mrb->ci = mrb->cibase + proc->env->cioff;
          break;
        default:
          /* cannot happen */
          break;
        }

ブロック中でbreakを実行した場合も命令コードはOP_RETURNになります。上記ではどこまで呼び出しを巻き戻るかを調整しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
        cipop(mrb);
        acc = ci->acc;
        pc = ci->pc;
        regs = mrb->stack = mrb->stbase + ci->stackidx;

各種実行時情報をメソッド呼び出し前に戻しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
-
|
!
        while (eidx > mrb->ci->eidx) {
          ecall(mrb, --eidx);
        }

ensure節の処理です。例外処理についてはまた今度説明します。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
-
|
|
!
        if (acc < 0) {
          mrb->jmp = prev_jmp;
          return v;
        }

Cで書かれたメソッドから呼ばれた場合、accは-1になっています。つまり、ここではCで書かれたメソッドに戻り値を返すということをしています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
 
 
 
 
 
 
!
|
!
        DEBUG(printf("from :%s\n", mrb_sym2name(mrb, ci->mid)));
        proc = mrb->ci->proc;
        irep = proc->body.irep;
        pool = irep->pool;
        syms = irep->syms;
 
        regs[acc] = v;
      }
      JUMP;
    }

各種実行情報復元その2。戻り値をレジスタに格納してメソッド呼び出しを行ったOP_SENDの次の命令から処理を再開しています。

OP_BLKPUSH

次にブロック周りを見てみましょう。yieldをコンパイルするとOP_BLKPUSHが埋め込まれることがわかります。

$ ./mruby.exe --verbose -e 'def foo; yield(1); end'
irep 117 nregs=5 nlocals=3 pools=0 syms=1
000 OP_ENTER    0:0:0:0:0:0:0
001 OP_BLKPUSH  R3      0:0:0:0
002 OP_LOADI    R4      1
003 OP_LOADNIL  R5
004 OP_SEND     R3      'call'  1
005 OP_RETURN   R3

というわけでOP_BLKPUSHを見てみます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
-
|
|
|
|
|
|
|
|
|
|
-
|
|
!
|
|
!
    CASE(OP_BLKPUSH) {
      /* A Bx   R(A) := block (16=6:1:5:4) */
      int a = GETARG_A(i);
      int bx = GETARG_Bx(i);
      int m1 = (bx>>10)&0x3f;
      int r  = (bx>>9)&0x1;
      int m2 = (bx>>4)&0x1f;
      int lv = (bx>>0)&0xf;
      mrb_value *stack;
 
      if (lv == 0) stack = regs + 1;
      else {
        struct REnv *e = uvenv(mrb, lv-1);
        stack = e->stack + 1;
      }
      regs[a] = stack[m1+r+m2];
      NEXT;
    }

m1, r, m2はyieldを実行するメソッドの引数情報です。オプション引数が含まれていないように見えますがm1は通常引数の数とオプション引数の数の合計になっています。lvはfor中でyieldすると0以外になるようです。ともかく、OP_BLKPUSHの目的はメソッド呼び出し時に積まれたブロックをレジスタに設定することです。忘れていると思いますが、mrubyのメソッド呼び出し規約ではメソッドを呼ぶ際に以下のようにself、引数、ブロックを積むことになっています。

regs→|self    |
      |引数1   |
      |・・・  |
      |引数n   |
      |ブロック|

stack[m1+r+m2]というのがちょうどブロックの位置を指すことになります。

OP_CALL

では次にブロックを呼び出す個所を見てみましょう。

004 OP_SEND     R3      'call'  1

タイトルにあるようにOP_CALLではなく、OP_SENDが使われています。が、嘘はついていません。どういうことかはcallメソッドの実装を見るとわかります。callメソッドはmrb_init_proc()で定義されています。

src/proc.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
void
mrb_init_proc(mrb_state *mrb)
{
  struct RProc *m;
  mrb_code *call_iseq = mrb_malloc(mrb, sizeof(mrb_code));
  mrb_irep *call_irep = mrb_calloc(mrb, sizeof(mrb_irep), 1);
 
  if ( call_iseq == NULL || call_irep == NULL )
    return;
 
  *call_iseq = MKOP_A(OP_CALL, 0);
  call_irep->idx = -1;
  call_irep->flags = MRB_IREP_NOFREE;
  call_irep->iseq = call_iseq;
  call_irep->ilen = 1;
 
  mrb->proc_class = mrb_define_class(mrb, "Proc", mrb->object_class);
 
  mrb_define_method(mrb, mrb->proc_class, "initialize", mrb_proc_initialize, ARGS_NONE());
 
  m = mrb_proc_new(mrb, call_irep);
  mrb_define_method_raw(mrb, mrb->proc_class, mrb_intern(mrb, "call"), m);
  mrb_define_method_raw(mrb, mrb->proc_class, mrb_intern(mrb, "[]"), m);
}

何をやっているかと言うと、callメソッドは呼ばれるとOP_CALLを実行するようになっています。というわけで今度はOP_CALLの処理を見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
-
|
|
|
|
|
|
|
|
|
-
-
|
!
-
|
!
!
|
|
-
|
|
|
|
|
|
|
!
-
|
|
|
|
|
|
-
|
!
-
|
!
|
|
|
|
!
!
    CASE(OP_CALL) {
      /* A      R(A) := self.call(frame.argc, frame.argv) */
      mrb_callinfo *ci;
      mrb_value recv = mrb->stack[0];
      struct RProc *m = mrb_proc_ptr(recv);
 
      /* replace callinfo */
      ci = mrb->ci;
      ci->target_class = m->target_class;
      ci->proc = m;
      if (m->env) {
    if (m->env->mid) {
      ci->mid = m->env->mid;
    }
        if (!m->env->stack) {
          m->env->stack = mrb->stack;
        }
      }
 
      /* prepare stack */
      if (MRB_PROC_CFUNC_P(m)) {
        mrb->stack[0] = m->body.func(mrb, recv);
        mrb->arena_idx = ai;
        if (mrb->exc) goto L_RAISE;
        /* pop stackpos */
        regs = mrb->stack = mrb->stbase + ci->stackidx;
        cipop(mrb);
        NEXT;
      }
      else {
        /* setup environment for calling method */
        proc = m;
        irep = m->body.irep;
        pool = irep->pool;
        syms = irep->syms;
        ci->nregs = irep->nregs;
        if (ci->argc < 0) {
          stack_extend(mrb, (irep->nregs < 3) ? 3 : irep->nregs, 3);
        }
        else {
          stack_extend(mrb, irep->nregs,  ci->argc+2);
        }
        regs = mrb->stack;
        regs[0] = m->env->stack[0];
        pc = m->body.irep->iseq;
        JUMP;
      }
    }

実行時情報をブロックのものに書き換えることでブロック呼び出しを行っています。env(REnv)にはブロック定義時の環境情報が入っています。それを使ってselfを書き換えているようですね。

環境情報を作っている部分も見ておきましょう。

$ ./mruby.exe -c --verbose -e 'foo { }'
irep 116 nregs=4 nlocals=2 pools=0 syms=1
000 OP_LOADSELF R2
001 OP_LAMBDA   R3      I(117)  2
002 OP_SEND     R2      'foo'   0
003 OP_STOP

というわけでOP_LAMBDAが引数2(OP_L_CAPTURE)で呼ばれるようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
-
|
|
|
|
-
|
!
-
|
!
|
|
|
!
    CASE(OP_LAMBDA) {
      /* A b c  R(A) := lambda(SEQ[b],c) (b:c = 14:2) */
      struct RProc *p;
      int c = GETARG_c(i);
 
      if (c & OP_L_CAPTURE) {
        p = mrb_closure_new(mrb, mrb->irep[irep->idx+GETARG_b(i)]);
      }
      else {
        p = mrb_proc_new(mrb, mrb->irep[irep->idx+GETARG_b(i)]);
      }
      if (c & OP_L_STRICT) p->flags |= MRB_PROC_STRICT;
      regs[GETARG_A(i)] = mrb_obj_value(p);
      NEXT;
    }

src/proc.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
 
-
|
|
|
-
|
|
|
|
|
|
!
-
|
!
|
|
!
struct RProc *
mrb_closure_new(mrb_state *mrb, mrb_irep *irep)
{
  struct RProc *p = mrb_proc_new(mrb, irep);
  struct REnv *e;
 
  if (!mrb->ci->env) {
    e = (struct REnv*)mrb_obj_alloc(mrb, MRB_TT_ENV, (struct RClass*)mrb->ci->proc->env);
    e->flags= (unsigned int)mrb->ci->proc->body.irep->nlocals;
    e->mid = mrb->ci->mid;
    e->cioff = mrb->ci - mrb->cibase;
    e->stack = mrb->stack;
    mrb->ci->env = e;
  }
  else {
    e = mrb->ci->env;
  }
  p->env = e;
  return p;
}

環境情報として、ブロック定義位置のスタックを記録していることがわかります。

OP_ADD

命令コードの最後にOP_ADDを見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
-
|
|
|
-
|
|
|
|
-
|
|
|
!
|
|
|
|
|
|
|
|
|
|
|
|
|
!
|
!
    CASE(OP_ADD) {
      /* A B C  R(A) := R(A)+R(A+1) (Syms[B]=:+,C=1)*/
      int a = GETARG_A(i);
 
      switch (TYPES2(mrb_type(regs[a]),mrb_type(regs[a+1]))) {
      case TYPES2(MRB_TT_FIXNUM,MRB_TT_FIXNUM):
        OP_MATH_BODY(+,i,i);
        break;
      case TYPES2(MRB_TT_FIXNUM,MRB_TT_FLOAT):
        {
          mrb_int x = regs[a].value.i;
          mrb_float y = regs[a+1].value.f;
          SET_FLOAT_VALUE(regs[a], (mrb_float)x + y);
        }
        break;
      case TYPES2(MRB_TT_FLOAT,MRB_TT_FIXNUM):
        OP_MATH_BODY(+,f,i);
        break;
      case TYPES2(MRB_TT_FLOAT,MRB_TT_FLOAT):
        OP_MATH_BODY(+,f,f);
        break;
      case TYPES2(MRB_TT_STRING,MRB_TT_STRING):
        regs[a] = mrb_str_plus(mrb, regs[a], regs[a+1]);
        break;
      default:
        i = MKOP_ABC(OP_SEND, a, GETARG_B(i), GETARG_C(i));
        goto L_SEND;
      }
      NEXT;
    }

何をやっているかというと、オペランドがともに数値だったらメソッド呼び出しを行わずに直接計算するということをしています。文字列についても通常のOP_SENDをバイパスして文字列連結を行うメソッドを直接呼び出すことで効率アップを行っています。

mrb_get_args(src/class.c)

Cで定義したメソッドがVMから引数を取得する場合、mrb_get_args()を使うと便利です。使用方法は以下のようになっています。

src/hash.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 
 
-
|
|
|
|
|
!
mrb_value
mrb_hash_aset(mrb_state *mrb, mrb_value self)
{
  mrb_value key, val;
 
  mrb_get_args(mrb, "oo", &key, &val);
  mrb_hash_set(mrb, self, key, val);
  return val;
}

各引数について、何型で引数を取得したいかを指定します。詳しくはmrb_get_args()のところに書かれているコメントならびにmrb_get_args()でgrepして得られる使用サンプルをご参照ください。(適当)

おわりに

というわけでmrubyのコード実行を見てきました。LOAD系などの単純な命令もあるのですが、やはりVM理解で鍵となるのはメソッド呼び出しの個所のようです。また、VMの実行モデルが何なのか知っておくことがソースを読み進めていく上で重要であると思いました。一瞬、スタックマシンに見てるのでYARVに関わっている人は要注意です:-P


*1 Riteはコードネームだから今後はmrubyと呼んで、とまつもとさんがつぶやいてたのでRiteVMと呼ばずにmrubyVMと呼びます
*2 スペルミスだと思うのだけど、何故修正されないのだろう?
*3 2012/6/10にDISPATCHに修正されましたがこのページは2012/5/24時点のソースを元に解説しているのでそのままにしておきます:-P
*4 ところでコメントだと引数レジスタの末尾がR(A+C-1)になってるけど、R(A+C)では?
*5 わかるとは思いますが念のため、Program Counterの略です

トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS