Rubyでは$で始まる変数はグローバル変数です。しかし、リファレンスマニュアルを見てみると
と書かれています。今回の読解対象はこれがどのようなカラクリになっているかです。ちなみにYARVのことがわかっていることを前提に話を進めますのでわかってない人はこのページなどを見てYARVの実行モデルを理解しておいてください。
なお、今回対象としたバージョンは
ruby 1.9.0 (2008-06-14 revision 16363)
です。
グローバル変数実装の概要はRHGを参照してください。要約すると、
ということになります。
Rubyではこの仕組みを利用して実体のないグローバル変数を定義することができます。例えば、
ruby.c
ruby_prog_init(void) { rb_define_virtual_variable("$-W", opt_W_getter, 0); opt_W_getter(VALUE val, ID id) { if (ruby_verbose == Qnil) return INT2FIX(0); if (ruby_verbose == Qfalse) return INT2FIX(1); if (ruby_verbose == Qtrue) return INT2FIX(2); return Qnil; /* not reached */ }
というようなことが可能です。
青木さんはグローバル変数のところを投げ遣り度85%で進行してますがこの仕組みはとてもおもしろいと思うのですけどね、まあ黒魔術なわけですが。
スレッドローカルなグローバル変数の例として$SAFEを見てみましょう。まず定義されているところです。
eval.c
Init_eval(void) { rb_define_virtual_variable("$SAFE", safe_getter, safe_setter);
eval_safe.c
safe_getter(void) { return INT2NUM(rb_safe_level()); } rb_safe_level(void) { return GET_THREAD()->safe_level; }
というわけでgetterの関数が呼ばれた場合、現在実行しているスレッドの構造体のsafe_levelが返されています。
余談ですがスレッド構造体なんてなかったRuby1.8まではsafe_levelはCレベルではグローバル変数でRubyのスレッド切り替えの時にスレッドコンテキストに保存されているsafe_levelが設定されるという仕組みになっていました。
ローカル変数の方はもう少し複雑です。例として$&を使います。
re.c
Init_Regexp(void) { rb_define_virtual_variable("$&", last_match_getter, 0); last_match_getter(void) { return rb_reg_last_match(rb_backref_get()); }
vm.c
rb_backref_get(void) { return vm_svar_get(1); }
svarという単語が出てきました。svar用の領域はスレッドスタックにローカル変数と一緒に存在しています。
|Qnil # for n |Qnil # for pi |Qnil # for svar dfp,lfp→|GC_GUARDED_PTR(0)
では先に進みます。
vm.c
vm_svar_get(VALUE key) { rb_thread_t *th = GET_THREAD(); return vm_cfp_svar_get(th, th->cfp, key); } vm_cfp_svar_get(rb_thread_t *th, rb_control_frame_t *cfp, VALUE key) { cfp = vm_normal_frame(th, cfp); return lfp_svar_get(th, cfp ? cfp->lfp : 0, key); } vm_normal_frame(rb_thread_t *th, rb_control_frame_t *cfp) { while (cfp->pc == 0) { cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); if (RUBY_VM_CONTROL_FRAME_STACK_OVERFLOW_P(th, cfp)) { return 0; } } return cfp; }
vm_insnhelper.c
lfp_svar_get(rb_thread_t *th, VALUE *lfp, VALUE key) { NODE *svar = lfp_svar_place(th, lfp); switch (key) { case 0: return svar->u1.value; case 1: return svar->u2.value; default: { const VALUE hash = svar->u3.value; if (hash == Qnil) { return Qnil; } else { return rb_hash_lookup(hash, key); } } } } lfp_svar_place(rb_thread_t *th, VALUE *lfp) { VALUE *svar; if (lfp && th->local_lfp != lfp) { svar = &lfp[-1]; } else { svar = &th->local_svar; } if (NIL_P(*svar)) { *svar = (VALUE)NEW_IF(Qnil, Qnil, Qnil); } return (NODE *)*svar; }
場合分けされてますが先ほどのsvar用に確保された領域を使うと思って問題ないでしょう。というわけで$&が参照された場合、ローカル変数領域のsvarが指すNODEのu2.valueが返されるようです。ちなみに、u1.valueは$_です。
とこんな感じなのですが実は$&参照時には上記のパスは通りません。YARVコードへのコンパイルでは$&が現れるとgetspecial命令が生成されます。
insns.def
DEFINE_INSN getspecial (VALUE key, rb_num_t type) () (VALUE val) { val = vm_getspecial(th, GET_LFP(), key, type); }
vm_insnhelper.c
vm_getspecial(rb_thread_t *th, VALUE *lfp, VALUE key, rb_num_t type) { VALUE val; if (type == 0) { VALUE k = key; if (FIXNUM_P(key)) { k = FIX2INT(key); } val = lfp_svar_get(th, lfp, k); } else { VALUE backref = lfp_svar_get(th, lfp, 1);
と、lfp_svar_get関数まで処理がバイパスされています。
今回はグローバル変数に見えるローカル変数の実装を見てきました。実装方法は以下のようになっています。
それではみなさんもよいコードリーディングを。