ひとり勉強会 RSSフィード

2006-10-27

YARVソースコード勉強会 (1)

| 05:20 |  YARVソースコード勉強会 (1)を含むブックマーク

金曜日は YARV: Yet Another Ruby VM のソースコード勉強会をやります。

YARVというのは、オブジェクト指向スクリプト言語 Ruby の実装のひとつです。ふつうのRubyと違って、いったんスクリプト仮想マシンバイトコードに変換して、高速実行するのが特徴らしいです。

最近は他にも Parrot, CLR, JavaVM などなど仮想マシンを使ったプログラミング言語処理系がアツいっぽい…。とゆーわけで、何かひとつは詳しくなってみようかとYARVに突撃します。あと、わたし、まだ10行以上のRubyスクリプトを書いたことがありません。YARVの勉強、兼、斜め下からのRuby入門、を狙っています。無謀にもほどがあります。

資料

| 05:20 |  資料を含むブックマーク

現時点での最新リリース 0.4.1 (revision 522) を読みます。なぜレポジトリの最新版を追いかけないかというと、手元の環境にbisonがなかったのでビルドが手間だったからです。ちょう手抜きです。

参考にしようと思ってるサイト:

YARV: Yet Another Ruby VM
本家本元です。ソースコードダウンロードはここから。アーキテクチャの説明なども載ってます。
YARV Maniacs
Rubyist Magazieの連載です。作者のささだこういちさん自らによる解説記事。ここで紹介されているような技術がソース上でどう実装されてるのかなー、っていうのを読んできたいです。
Rubyソースコード完全解説
青木峰郎さんによる、Rubyのソースコード解説書です。YARVのコア以外の、ふつうのRubyと共通なところについては、こっちに頼りながら行きます。

というわけで、はじまりはじまり〜。

main @ main.c

| 05:20 |  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

| 05:20 |  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();
  ... 略 ...
}

Init_yarv!YARVが出てきました!

でもその前に、すぐあとの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

| 05:20 |  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

| 05:20 |  Init_yarvcore @ yarvcore.cを含むブックマーク

この関数は、基本的には、YARVRubyスクリプトからさわれるようにするための、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

| 05:20 |  ruby_option @ eval.cを含むブックマーク

初期化が終わって、次にmainから呼ばれる処理はコマンドラインオプションの解析です。

おもしろそうなところは特になかったので、スキップ。

ruby_run @ eval.c

| 05:20 |  ruby_run @ eval.cを含むブックマーク

最後に、main から ruby_run が呼ばれます。その中では何段にも関数が呼ばれますが、メインの流れは一列です。

  • main
    • ruby_run
      • ruby_exec
        • ruby_exec_internal
          • yarvcore_eval_parsed @ yarvcore.c

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で実行しています。いよいよここから実行!

まとめ

| 05:20 |  まとめを含むブックマーク

というところで、今週のYARVソースコード勉強会はお開きとなります。(^^)

来週は、compile側とeval側のどっちに進んだ方がいいんだろ?データ構造のチェックとかが先かなあ。。。

あとで読む

  • PUSH_TAG, EXEC_TAG 等のタグジャンプの実装
  • th_init2 : Rubyスレッドを一つ作成する関数、の、詳細
  • Init_yarv で作られるスレッドの意味

ささだささだ 2006/10/27 09:21 凄い企画ですねぇ。thread/vm オブジェクトを作り直しているのは、まず最初はRubyオブジェクトを作成する環境がないので、インチキthread/vmオブジェクトを作っている、という感じです。その後、だいたい環境が出来た後でRuby レベルでアクセスできるThread/VM オブジェクトを作っている、という感じです。

*_TAG については、考え方は従来のRubyと同じです。管理する情報がちょっと違います。

hzkrhzkr 2006/10/27 21:47 わわわ。コメントありがとうございます&YARVという素晴らしいソフトをありがとうございます!尻すぼみにならないように頑張ります。

なるほど、Init_yarvの段階ではまだRubyの他のパートが初期化されてないので、オブジェクトは作れないわけですね。

トラックバック - http://d.hatena.ne.jp/hzkr/20061027

CC0
To the extent possible under law, the person who associated CC0 with this work has waived all copyright and related or neighboring rights to this work.