ひとり勉強会

ひとり楽しく勉強会

th_eval_body:実行

th_set_top_stack で制御フレームの設定が終わったら、いよいよ実行開始。th_eval_body 関数です。

VALUE
th_eval_body(yarv_thread_t *th)
{
  ...
  TH_PUSH_TAG(th);
  if ((state = EXEC_TAG()) == 0) {
  vm_loop_start:
    result = th_eval(th, initial);
    if ((state = th->state) != 0) {
      err = result;
      th->state = 0;
      goto exception_handler;
    }
  }
  else {
    ...
  exception_handler:

実際の YARVマシン語の実行で、th_eval 関数で行われるみたいです。普通の実行フローに関しては、この関数は th_eval を呼び出すだけ。ただしこちらには、全ての例外処理が集約されています。th_eval 内でRubyの例外を扱うときは、th->stateに0以外の値を設定してreturnしてきます。んで、goto exception_handler で例外処理へ。処理が済んだらまた goto vm_loop_start で th_eval に戻るという流れですね。ジャンプタグ で例外処理に移る場合もあるみたいです。
とりあえず例外処理の話はむずかしそうなので後回しにして、通常フローの th_eval の方を先に調べてみます。

th_eval @ th_evalbody.ci

この関数の中身は、make の時に、YARVマシン語定義ファイルから自動生成されます。

VALUE
th_eval(rb_thread_t *th, VALUE initial)
{
    INSN_DISPATCH();
  /******************/
#include "vm.inc"
  /******************/
    END_INSNS_DISPATCH();
}

INSN_DISPATCH @ vm.h は、スレッデッドコード最適化をしない場合はこういうswitch文で

#define INSN_DISPATCH()         \
  while(1){                     \
    switch(GET_CURRENT_INSN()){

する場合は

#define INSN_DISPATCH()     \
  TC_DISPATCH(__START__)    \
  {
#define TC_DISPATCH(insn) \
  DISPATCH_ARCH_DEPEND_WAY(GET_CURRENT_INSN()); \
  INSN_DISPATCH_SIG(insn); \
  goto *GET_CURRENT_INSN(); \
  ;

先頭の命令のラベルへジャンプするgoto文になるようです。INSN_DISPATCH_SIG はアセンブラに落としたときに読みやすいようにコメントかな。

#define INSN_DISPATCH_SIG(insn) \
  asm volatile (  "; #[end  ] " # insn "\n"\
                "\t; #==================================================\n") \

DISPATCH_ARCH_DEPEND_WAY では、Cのgoto文ではなくて直接x86マシン語でjmpしているみたい。YARV Maniacs 第3回で触れられている、gcc 3.3.x for x86 ではうまくいかないという件の対処ですね。

#if __GNUC__ && (__i386__ || __x86_64__) && __GNUC__ == 3
  #define DISPATCH_ARCH_DEPEND_WAY(addr) \
    asm volatile("jmp *%0;\t# -- inseted by vm.h\t[length = 2]" : : "r" (addr))

これ以外の環境では空になるマクロ定義になってました。
次に include されている "vm.inc" は、想像どおり、スレッデッドコード最適化がOFFの時はcase ...:の列に、ONのときは普通のラベルの列に展開されるコードになっています。オプションに応じて、どっちか適切な方を出すマクロ INSN_ENTRY が定義されています。

/* BIN : Basic Instruction Name */    // @ insns.inc
#define BIN(n) YARVINSN_##n           // @ insns.inc

#define INSN_ENTRY(insn) \
case BIN(insn):
#define LABEL(x)  INSN_LABEL_##x
...
#define INSN_ENTRY(insn) \
  LABEL(insn): \
  INSN_ENTRY_SIG(insn); \

INSN_ENTRY_SIG は INSN_DISPATCH_SIG と同じようにasmのコメントを生成するマクロ。
"vm.inc" の中身はこれを使って、

INSN_ENTRY(nop){
  ...コード...
}
INSN_ENTRY(getlocal){
  ...コード...
}

ひたすら各命令のCによる実装コードが並んでいます。ただ、どの命令も基本的に

  1. (必要なら)マシン語からオペランドを取得
  2. (必要なら)スタックからVALUEをPOP
  3. 処理
  4. (必要なら)スタックにVALUEをPUSH

という定型の流れがあるので、3 の処理の部分を書いた命令定義ファイルから、他の部分が全部自動生成される仕組みになっています。具体的には、"insns.def" というファイルを元に、"tool/insns2vm.rb" というスクリプトが "vm.inc" 他必要なコードを全部生成しています。

insns.def

insns.def自体の頭にコメントで形式が定義されてますが

DEFINE_INSN
命令名
(命令オペランド, ..)
(スタックからPOPする値, ..)
(返値(PUSHする値))
{
   .. // 本体の実行コード
}

こういう形式で命令を定義します。nop命令はこんなんに

DEFINE_INSN
nop
()
()
()
{
    /* none */
}

なってました。

DEFINE_INSN
getlocal
(lindex_t idx)
()
(VALUE val)
{
    val = *(GET_LFP() - idx);
}

getlocalだとこんなかんじです。

ちょっと遊び

見た感じ、マシン語を勝手に追加しちゃう遊びは凄く簡単にできそうですね。insns.def に下のような数行を追加してみる実験。

DEFINE_INSN
hello
()
()
()
{
    puts("Hello, world!")
}

ちょっと puts は酷い気が自分でもしますすみません。まあいいや。この命令を使ってみるには、、、構文解析からいじるのは面倒なので、空配列を書いたらhello命令を出力するようにしてしまえ!compile.cを書き換えちゃいます。

case NODE_ZARRAY:{
  ADD_INSN(ret, nd_line(node), hello); // ここ追加
  if (!poped) {
    ADD_INSN1(ret, nd_line(node), newarray, INT2FIX(0));
  }
  break;
}

で、もいちどmakeしなおして。

> ruby -e []
Hello, world!

やった!HelloWorldが2バイトで書けた!
m(_ _)m
こういうふざけた遊びは置いておくにしても、実際、いろいろ自分で改造してみるのもできそうですね。もうすぐひととおりYARVのソースは読み終わりそうなので、終わったら色々改造して遊ぶ勉強会にしてみようかな。

th_eval_body:例外処理

改めて話を戻して、例外処理について。raise〜rescueやthrow〜catchの他にも、break/redo/next, returnなども同じ仕組みを使って実装されています。この辺りの処理は th_eval_body にまとまっています。基本的な流れとしては…

exception_handler:
  cont_pc = cont_sp = catch_iseqval = 0;

  cfp = th->cfp;                            // 現在のCFP
  epc = cfp->pc - cfp->iseq->iseq_encoded;  // 例外の発生したアドレス

  // 現在のスコープで例外をcatchすべきかどうかチェック
  //  (iseq->catch_tableを調べる)

  // catch すべきなら例外タイプに応じて処理

  // そうでない場合、もう一つ上のスコープを調べる
  else {
    th->cfp++;
    if (th->cfp->pc != &yarv_finish_insn_seq[0]) {
      goto exception_handler;
    }
    else {
      pop_frame(th);
      th->errinfo = err;
      TH_POP_TAG2();
      JUMP_TAG(state);
    }
  }

&yarv_finish_insn_seq[0] は前回読んだfinish命令が入ったスコープですね。そこまで言っても例外ハンドラが見あたらなかったら、VMレベルでもっと上にエラーを投げてます。
「現在のスコープで例外をcatchすべきかどうかチェック」は以下のような単純なforループで探索。ensureハンドラが見つかった場合、そこに処理を移します。

for (i = 0; i < cfp->iseq->catch_table_size; i++) {
  entry = &cfp->iseq->catch_table[i];
  if (entry->start < epc && entry->end >= epc) {
    if (entry->type == CATCH_TYPE_ENSURE) {
      catch_iseqval = entry->iseq;
      cont_pc = entry->cont;
      cont_sp = entry->sp;
      break;
    }
  }
}

ensureに処理を移すコードはこんな感じに、新しく制御フレームを作ってvm_loop_startに戻ってます。

if (catch_iseqval != 0) {
  /* found catch table */
  rb_iseq_t *catch_iseq;

  /* enter catch scope */
  GetISeqPtr(catch_iseqval, catch_iseq);
  cfp->sp = cfp->bp + cont_sp;
  cfp->pc = cfp->iseq->iseq_encoded + cont_pc;

  /* push block frame */
  cfp->sp[0] = err;
  push_frame(th, catch_iseq, FRAME_MAGIC_BLOCK,
    cfp->self, (VALUE)cfp->dfp, catch_iseq->iseq_encoded,
    cfp->sp + 1, cfp->lfp, catch_iseq->local_size - 1);

  state = 0;
  th->errinfo = Qnil;
  goto vm_loop_start;
}

catch_tableでCATCH_TYPE_ENSURE以外の適切なタイプにひっかかった場合、たとえばbreak中にCATCH_TYPE_BREAKの範囲に当たった場合は、単純にその位置にpcを戻せば良いので、新しいフレームを積んだりせずに(ほぼ即)goto vm_loop_startで戻ってました。

まとめ

しばらく書いてなかったらひとり勉強会の書き方を忘れています(あせ。なんか変な感じ。
次回は、insns.def の内容を面白そうなところをつまみ食いしながら一気に眺めてみようと思います。あと読んでみたいのは、スレッド周りの実装くらいでしょうか。というわけで、あと1回か2回でYARVソースコード勉強会を第一部完とする予定です。たぶん第二部はYARVを改造して遊ぶ会。