Ruby のスタックフレーム

スタック

普通の、関数(またはメソッド)を主体とした言語では、関数呼びだし(など)のたびに、 ローカル変数や戻る場所など関数一つの実行のために必要な情報を構造体に格納し、 それをスタックに積む。関数から戻るとスタックの先頭の構造体が pop されて実行が 前のメソッドに戻る。

ruby の実行もやはりメソッドの連鎖に他ならないから、やはり同様の実行イメージを 持っている。ただし以上のような単純なイメージはあくまで「概要は」そうであるという 話であって、実際にはもっと複雑なことになっている。それというのも Ruby には イテレータのように実行の経路自体をスクリプトから変化させる機能があるため、 単純なスタックの push/pop だけでは実装できないからだ。そのためスタックに 積む構造体もひとつでなく機能ごとに分割されており、実行中に複雑に積みかえられる。 具体的にはその構造体は struct FRAME、struct SCOPE、struct BLOCK の三つだ。 まずはこれら構造体について簡単に解説しておく。

struct FRAME

struct FRAME はメソッド呼びだし一回につき一つ生成される。


struct FRAME {
    VALUE self;         /* メソッドの self */
    int argc;           /* 引数の個数と値の配列 */
    VALUE *argv;
    ID last_func;       /* このメソッド内部で呼んだ最後のメソッド */
    VALUE last_class;   /* 同じくそのレシーバのクラス */
    VALUE cbase;        /* 「外」のクラス */
    struct FRAME *prev; /* 呼びだした側のメソッドのフレーム */
    struct FRAME *tmp;  /* GC よけ、後述 */
    char *file;         /* メソッドがあるファイル名 */
    int line;           /* メソッドの開始行 */
    int iter;           /* イテレータかどうか */
};

(env.h、定義だけを抜きだし)

だいたいはコメントを見ればわかると思うが、tmp と cbase についてだけ 追加説明しておく。struct FRAME は主にメソッド呼びだしで生成されるのだが、 module_eval などのように一時的にスコープを変更してコードが実行される時には、 一時的に FRAME を置き換えて実行する。tmp はその時に元の FRAME が回収されて しまわないようポインタを格納しておくために使われる。また、cbase はその メソッドが定義された時にまわりにあったクラスを指すポインタである。 通常はインスタンスメソッドはそのクラス定義の中で定義されるので cbase == self.type が成立するのだが、しかし例えば以下のような場合どうなる だろうか。


  class A
    B = 5
  end
  a = A.new
  def a.test
    puts B
  end

この場合メソッド定義のまわりにはクラス定義がない。従って cbase は A でなく Object になる。定数の検索はまずはこの「外」のクラスに対しておこなわれ、 続いて上位クラスを調べることになっているので、このようなパラメータが必要に なるのだ。

このような仕様は制限のように見えるかもしれないが、例えば以下のような状況では 非常に便利なのである。


  class A
    C = 5
    def A.new
      puts C
      super
    end
  end

外のクラスが検索されない場合 A.new からは A の定数 (C など) はそのままは 参照できず、::A::C などと書く必要がある。

struct BLOCK

struct BLOCK は (Ruby の) ブロックの実体であり、動的に割りあてられる 変数の領域などを持つ。Proc クラスがこの構造体のラッパーのようなものだ。


struct BLOCK {
    NODE *var;               /* ブロック引数の名前のリスト */
    NODE *body;              /* ブロック本体の構文木 */
    VALUE self;              /* ブロック生成元の self */
    struct FRAME frame;      /* ブロック生成元のメソッドのフレーム */
    struct SCOPE *scope;     /* ブロック生成時のスコープ */
    VALUE klass;             /* ブロック生成元の self のクラス */
    struct tag *tag;         /* 戻る時の識別情報 */
    int iter;                /* イテレータか? */
    int vmode;
    int flags;
    struct RVarmap *d_vars;  /* ブロック変数 */
    VALUE orig_thread;       /* ブロックを生成したスレッド */
    struct BLOCK *prev;      /* ブロック生成時のひとつ前のブロック */
};

(eval.c、コメント付加)

frame がポインタではないことに注目。これはつまり struct FRAME の 内容をまるごとコピーして保持するということである。理由はまだ調査中だが、 struct FRAME が VALUE でないことが関係しているのではないかと思う。 つまり FRAME は GC の対象にならないので、適当にわりあててほおっておくことが できないからだ。ではなぜ VALUE にしない (DATA でラップしない) のかというと…?

struct SCOPE

ローカル変数を保持するための構造体。 メソッド・クラス・モジュール定義がひとつのスコープである。 (クラス定義中にも普通に文が書けるのは知ってるよね?)


struct SCOPE {
    struct RBasic super;
    ID *local_tbl;        /* ローカル変数名の配列 */
    VALUE *local_vars;    /* ローカル変数の値 (VALUE) の配列 */
    int flag;             /* 配列の配置方法を示すフラグ */
};

(env.h、定義だけ抜きだし)

最初の要素が struct RBasic だからこれは VALUE になれる。なぜ VALUE に する必要があるかというと、これまたイテレータ(proc)のためである。 Proc が生成されるとその Proc はメソッドよりも長生きする可能性があり、 その Proc からはローカル変数が参照できるので、Proc がすべて消滅するまで ローカル変数は保持しておかねばならない。つまり GC の対象にしないと解放の 時期がわからないのである。またこのことが struct SCOPE が struct FRAME から 切りはなされている理由でもある。またもう一つの理由は C で定義したメソッドを 呼び出すときには SCOPE を積まないからだ。C で定義したメソッドは Ruby の ローカル変数など必要ないから、これは当然である。

ジャンプ

スタックに対する push/pop だけで実行が描ければよいのだが、実際には Ruby には例外や catch/throw といった機構があり、これはスタックに積まれた フレームを一気にいくつも飛びこえていく必要がある。このような機能は C の setjmp longjmp を使って実装されている。 また現在の Ruby では一つのフレーム内でも break や return、next などの 制御構造も同じ機構を使って実装している。

その概要はこうだ。まずメソッドコールや begin while イテレータなどの 開始時に必ず setjmp する。そして実行中に raise や break があったら 特定の値を返して longjmp する。そうすると最後に setjmp したところまで 戻ってくるので、ここで自分が待っていたジャンプかどうか判定して実行経路を 変える。もし自分が待っているのではないジャンプだったら再度 longjmp する。 「自分が待っているジャンプ」というのは例えば関数呼び出し時の setjmp に とっては return だとか retry である。rescue にとっては raise のジャンプ である。while にとっては break や next である。

この setjmp した時の jmp_buf をとっておく場所が struct tag だ。


struct tag {
    jmp_buf buf;
    struct FRAME *frame;
    struct iter *iter;
    ID tag;
    VALUE retval;
    struct SCOPE *scope;
    int dst;
    struct tag *prev;
};
static struct tag *prot_tag;

(eval.c)

prev はやはりリンクリストをつくるためのポインタである。全ての tag は このポインタによってリストになる。そのリストをつくるためのマクロが PUSH_TAG POP_TAG だ。

#define PUSH_TAG(ptag) {                \
    struct tag _tag;                    \
    _tag.retval = Qnil;                 \
    _tag.frame = ruby_frame;            \
    _tag.iter = ruby_iter;              \
    _tag.prev = prot_tag;               \
    _tag.retval = Qnil;                 \
    _tag.scope = ruby_scope;            \
    _tag.tag = ptag;                    \
    _tag.dst = 0;                       \
    prot_tag = &_tag;

#define POP_TAG()                       \
    if (_tag.prev)                      \
        _tag.prev->retval = _tag.retval;\
    prot_tag = _tag.prev;               \
}

(eval.c 編集)

ptag は以下の三種類で、タグをおおまかに分類するために使われるものである。 というのはこの tag による setjmp/longjmp 機構はただのジャンプの他に スレッドの実装にも使われるので、下手にスレッドを超えてジャンプして しまったりするととんでもないことになるからである。PROT_FUNC はその名の 通りメソッドコール専用である。(しかしなぜか for の前にも使われている…)

#define PROT_NONE   0
#define PROT_FUNC   -1
#define PROT_THREAD -2

(eval.c)

実際の setjmp/longjmp はマクロ EXEC_TAG と JUMP_TAG にラップされている。 EXEC がジャンプではなくて setjmp の実行であることに注意。

#define EXEC_TAG()    setjmp(prot_tag->buf)

#define JUMP_TAG(st) {                  \
    ruby_frame = prot_tag->frame;       \
    ruby_iter = prot_tag->iter;         \
    longjmp(prot_tag->buf,(st));        \
}

(eval.c)

先程述べたとおりジャンプする時には特定の返り値を返す。これによって setjmp 側では「待っている」ものと「待っていない」ものを識別するのだった。 それは以下の定数で示される。( JUMP_TAG(TAG_RETURN) のように使う。)

#define TAG_RETURN      0x1
#define TAG_BREAK       0x2
#define TAG_NEXT        0x3
#define TAG_RETRY       0x4
#define TAG_REDO        0x5
#define TAG_RAISE       0x6
#define TAG_THROW       0x7
#define TAG_FATAL       0x8
#define TAG_MASK        0xf

(eval.c)

struct RVarmap

ブロックローカルな変数の置き場所。ブロックローカルな変数は eval.c では dvar (dynamic variable)と表記される。


struct RVarmap {
    struct RBasic super;
    ID id;                    /* 変数名 */
    VALUE val;                /* 値 */
    struct RVarmap *next;     /* リンク */
};

(env.h、定義だけ抜きだしコメントをつけた)

これも最初に struct RBasic があるので VALUE になれる。 また next があることからブロック変数はリンクリストで保持されて いることがわかる。