YARVソースコード勉強会 (1)
金曜日は YARV: Yet Another Ruby VM のソースコード勉強会をやります。
YARVというのは、オブジェクト指向スクリプト言語 Ruby の実装のひとつです。ふつうのRubyと違って、いったんスクリプトを仮想マシンのバイトコードに変換して、高速実行するのが特徴らしいです。
最近は他にも Parrot, CLR, JavaVM などなど仮想マシンを使ったプログラミング言語の処理系がアツいっぽい…。とゆーわけで、何かひとつは詳しくなってみようかとYARVに突撃します。あと、わたし、まだ10行以上のRubyスクリプトを書いたことがありません。YARVの勉強、兼、斜め下からのRuby入門、を狙っています。無謀にもほどがあります。
資料
現時点での最新リリース 0.4.1 (revision 522) を読みます。なぜレポジトリの最新版を追いかけないかというと、手元の環境にbisonがなかったのでビルドが手間だったからです。ちょう手抜きです。
参考にしようと思ってるサイト:
- YARV: Yet Another Ruby VM
- 本家本元です。ソースコードのダウンロードはここから。アーキテクチャの説明なども載ってます。
- //jp.rubyist.net/magazine/?0006-YarvManiacs">YARV Maniacs:Rubyist Magazieの連載です。作者のささだこういちさん自らによる解説記事。ここで紹介されているような技術がソース上でどう実装されてるのかなー、っていうのを読んできたいです。
- //i.loveruby.net/ja/rhg/">Rubyソースコード完全解説:青木峰郎さんによる、Rubyのソースコード解説書です。YARVのコア以外の、ふつうのRubyと共通なところについては、こっちに頼りながら行きます。
というわけで、はじまりはじまり〜。
main @ main.c
今日は、mainから初期化が済んで、スクリプトの実行がいよいよ始まる!その手前までの流れを読んでいきます。まずはmain関数から行ってみましょう。ここからRuby/YARVの実行がはじまります。環境依存の#ifdefを取っ払うと、こんな感じのシンプルなmainです。
int main(int argc, char **argv, char **envp) { { RUBY_INIT_STACK ruby_init(); ruby_options(argc, argv); ruby_run(); } return 0; }
最初の RUBY_INIT_STACK は、ガベコレのときに必要になる、スタックの開始位置を覚えるためのマクロです。やることは要するに、スタックの開始アドレスを rb_gc_stack_start に記録するだけです。(IA-64環境だとちょっと別の変数に記録するみたいです。)実際、マクロの実装は
#define RUBY_INIT_STACK \ VALUE variable_in_this_stack_frame; \ ruby_init_stack(&variable_in_this_stack_frame);
こうで、スタックに適当な変数を作って、そのアドレスを ruby_init_stack に渡してます。ruby_init_stack はスタックの成長方向チェックなどで少し複雑になってますが、要は
void ruby_init_stack(VALUE *addr)
{
... 略 ...
rb_gc_stack_start = addr;
... 略 ...
}
こうです。この辺りは、後々GCのコードを読むときにもいっぺん見るかもしれません。今日は深入りせずにおしまいにします。
続いて、3つの処理が順番に呼び出されています。
順に追っていきますね。
ruby_init @ eval.c
初期化関数、ruby_init のメインな部分を抜き出してみました。
void ruby_init() { ... 略 ... Init_yarv(); Init_stack((void *)&state); Init_heap(); PUSH_TAG(PROT_NONE); if ((state = EXEC_TAG()) == 0) { rb_call_inits(); ruby_prog_init(); ALLOW_INTS; } POP_TAG_INIT(); ... 略 ... }
でもその前に、すぐあとのif文の条件が、はじめてRubyのソースを見る目には謎です。
PUSH_TAG(PROT_NONE); if ((state = EXEC_TAG()) == 0) { ... } POP_TAG_INIT();
うーん?EXEC_TAGの定義を見てみよう。
#define TH_EXEC_TAG() \ (FLUSH_REGISTER_WINDOWS, ruby_setjmp(_th->tag->buf)) #define EXEC_TAG() \ TH_EXEC_TAG()
ruby_setjmpはsetjmpか_setjmpの#defineでした。ということで、ここではsetjmpが呼ばれているみたいです。setjmpは、最初は0を返して、中でlongjmpが呼ばれると0以外の値で戻ってくるはずです。
if ((state = EXEC_TAG()) == 0) { // ※ // 最初はEXEC_TAG==setjmpが0を返すので、ここに来る ... // この辺りでエラーが起きたときは中でlongjmpが呼ばれる // すると※にジャンプして EXEC_TAG が 0 以外を返すので ... } // ここに処理がうつることになる
これは、begin〜rescue みたいな例外処理を、C言語でやってるってことでしょう。例外状態まで気にすると大変そうなので、ひとまずここの if (...) は見なかったことにして先に進みます。エラーなんて起きません!
(※ Ruby Hacking Guideをちょっと見てみたところ、この ***_TAG でいろいろジャンプする「ジャンプタグ」という仕組みはRuby評価期ではたっぷりと使われているそうです。YARVでどうなっているのかはまだわかりません。使っていそうだったら、その時にまた戻ってきて調べようと思います。)
さて、ruby_initが呼び出す初期化ルーチンのうち、YARVに直接関係ありそうな部分は二つしかありません。最初に呼ばれているInit_yarvと、rb_call_initsの中で呼ばれているInit_yarvcoreです。
- Init_yarv
- rb_call_inits
- ...
- Init_yarvcore
- ...
他はふつうのRubyの組み込みオブジェクトやモジュールの初期化っぽかったので、この勉強会ではどんどん飛ばしてっちゃいます。
Init_yarv @ yarvcore.c
一個目の初期化ルーチン Init_yarv はこんなんでした。
void Init_yarv(void) { /* initialize main thread */ yarv_vm_t *vm = ALLOC(yarv_vm_t); yarv_thread_t *th = ALLOC(yarv_thread_t); vm_init2(vm); theYarvVM = vm; th_init2(th); th->vm = vm; yarv_set_current_running_thread_raw(th); }
YARVの仮想マシンを表す構造体(yarv_vm_t型)と、スレッドを表す構造体(yarv_thread_t型)を1個ずつ作って初期化しています。vm_init2 はメモリをゼロクリアしているだけでした。th_init2 は、なにやらフレームのスタックを作成したりしてましたが、まだYARVの実行処理部分を見ていないので、どういう意味があるのかちゃんとつかめませんでした。のちのちスレッドの構造を見る段階で詳しくチェックします。
とりあえず、「仮想マシンを表す構造」と「スレッドを表す構造」が1個ずつ最初に作られることだけ覚えておきます。仮想マシンはtheYarvVM変数(GET_VM()マクロでアクセス可能)に、スレッドはyarv_set_current_running_thread_rawのなかでyarvCurrentThread変数(GET_THREAD()マクロでアクセス可能)に格納されています。
Init_yarvcore @ yarvcore.c
この関数は、基本的には、YARVをRubyスクリプトからさわれるようにするための、Rubyライブラリを提供するものです。
mYarvCore = rb_define_module("YARVCore"); rb_define_const(mYarvCore, "VERSION", rb_str_new2(yarv_version)); ... 略 ...
こんなのが延々と続いています。YARVをRubyから操作する方法には今のところ興味がないので、この辺りはさらっと読み飛ばしちゃえ〜!と思いきや、さりげなく
idAnswer = rb_intern(
"the_answer_to_life_the_universe_and_everything");
こんなのが混じってて面白かったです。
それはともかく、ライブラリの提供と思ってたら、途中で
// make vm /* create main thread */
のようなコメントがでてきはじめて戸惑いました。さっきvmやthreadは作らなかった??
念のため、make vm以降をちゃんと見てみました。
/* create vm object */
VALUE vmval = vm_alloc(cYarvVM);
yarv_vm_t *vm;
yarv_thread_t *th;
vm = theYarvVM;
xfree(RDATA(vmval)->data);
RDATA(vmval)->data = vm;
vm->self = vmval;
vm_alloc(cYarvVM) で、RubyのYarvVMクラスのインスタンス、vmvalを作っています。そこに、さっき作ったtheYarvVMをセットしたり、逆にtheYarvVMにvmvalをセットしたりしていますね。つまり、さっき作ったtheYarvVMは、YARV内部で使う仮想マシンの表現です。ここで作るvmvalは、それをRubyスクリプトに見せるためのラッパーオブジェクトみたいなのということでしょう。「vmを意味する(Rubyでの)オブジェクトを作る」処理。
続いて、create main thread というコメントの箇所です
/* create main thread */
vm->main_thread_val = yarv_thread_alloc(cYarvThread);
GetThreadPtr(vm->main_thread_val, th);
vm->main_thread = th;
vm->running_thread = th;
GET_THREAD()->vm = vm;
thread_free(GET_THREAD());
th->vm = vm;
yarv_set_current_running_thread(th);
同じように、yarv_thread_alloc(cYarvThread) でYarvThreadクラスのインスタンスを作っています。しかしここで、姉さん、事件です!
- yarv_thread_alloc
- thread_alloc
- thread_init
- th_init
yarv_thread_allocの中では、またth_initで新しくスレッドを表す構造を作っています。そのうしろでは、さっき作ったスレッドGET_THREAD()をthread_freeで解放して、今新しく作ったスレッドを yarv_set_current_running_thread で再設定しているように見えます。
ううむ、なんででしょう。
- Init_yarvcoreは繰り返し呼ばれる可能性があって、呼ばれた時点で前のスレッドは破棄するという動作になっている
というのはありそうですし、納得がいきます。でもそうすると、Init_yarvで一度スレッドを作ってるのはなんでだろ?ちょっとまだわからないので、これは課題としてとっておきます。
とりあえずここでは、「YARVのVMを表すRubyオブジェクトやスレッドを表すRubyオブジェクトを初期化して、組み込みオブジェクトとして提供している」ということで。
ruby_option @ eval.c
初期化が終わって、次にmainから呼ばれる処理はコマンドラインのオプションの解析です。
おもしろそうなところは特になかったので、スキップ。
ruby_run @ eval.c
最後に、main から ruby_run が呼ばれます。その中では何段にも関数が呼ばれますが、メインの流れは一列です。
yarvcoreにたどり着きました!
VALUE
yarvcore_eval_parsed(NODE *node, VALUE file)
{
VALUE iseq = th_compile_from_node(GET_THREAD(), node,
file);
return yarvcore_eval_iseq(iseq);
}
引数nodeには、ruby_optionsかどこかでいつの間にかparseされていた構文木が渡ってきます。引数fileには、スクリプトのファイル名が入っています。
関数の中身は読みやすいですね。構文木をiseq(Instruction Sequenceかな?)にコンパイルして、それを、yarvcore_eval_iseqで実行しています。いよいよここから実行!