Ruby
コードリーディング

CRuby のおすすめソースコードリーディング方法

More than 1 year has passed since last update.


TL;DR

CRuby のソースコードリーディングをする時は、parse.y, compile.c, insns.def のどれかから読み始めるのがオススメです。

特定のクラスの挙動が読みたいという話なら、そのクラスが定義されたファイル(例えば String なら string.c)を読むと良いです。

cf. https://speakerdeck.com/south37/ruby-sosukodofalsebu-kifang

cf. https://github.com/ruby/ruby

以下、詳しい説明です。


CRuby のソースコードリーディングの進め方

CRuby のソースコードリーディングの進め方について。

CRuby のソースコードを読み進める時は、main関数から始めるのも良いですが、 parse, compile, runtime のどのフェーズに興味があるかに応じて、parse.y, compile.c, insns.def のどれかから読み始めるのが個人的にはオススメです。

それぞれ、以下の役割を果たすファイルになっています。


  • parse.y ... ソースコードから抽象構文木(Abstract Syntax Tree, AST)を作る parser の実装が書かれている。bison という parser generator 向けのDSL(BNF + token 定義のオレオレ syntax)で書かれてるので、bisonの知識があると読みやすい。

  • compile.c ... AST を走査してYARV(Yet Another Ruby VM, Ruby 1.9から搭載されてる CRuby の VM)用の命令列を生成する処理が書かれてる。

  • insns.def ... YARV命令列が実際に行う処理の内容が書かれている。DSLで書かれていて、vm.inc, insns.inc, insns_info.inc などが生成される。

上記のファイルはRubyのコア部分が記載されてるファイルですが、「特定のクラス、メソッドの挙動」に興味がある場合、それぞれのクラスが定義されたファイルを見ると良いです。以下に例をいくつかあげます。


  • object.c ... BasicObject, Object, Module, Class, NilClass, Data, TrueClass, FalseClass が定義されています。

  • string.c ... String, Symbol クラスが定義されています。

  • array.c ... Array クラスが定義されています。

クラスが定義されてるファイルをリストアップすると、以下の様にたくさんあります。vm_* というファイルは、後述しますが YARV 周りの処理も記載されています。


  • array.c, class.c, complex.c, cont.c, dir.c, encoding.c, enumerator.c, error.c, file.c, hash.c, io.c, object.c, numeric.c, random.c, proc.c, rational.c, parse.y, re.c, struct.c, string.c, thread_sync.c, thread.c, time.c, transcode.c, vm.c, vm_eval.c, vm_trace.c


CRuby のそれ以外のファイル

上記であげたファイル以外で、重要そうなファイルをあげていくと以下になります。


  • ruby を実行した時の処理が記載されたファイル


    • main.c, ruby.c, eval.c など



  • YARV の処理が記載されたファイル


    • vm_* から始まるファイル群

    • vm.c は YARV 命令列実行の本体処理 vm_exec が定義されたファイル、vm_exec.c は insns.def から生成された vm.inc が埋め込まれるファイル

    • vm_insnhelper.c は insns.def から呼び出される helper 関数が中心だが、結局色々な箇所から呼ばれてる



  • Memory Allocation, GC 周りの処理が記載されたファイル


    • gc.c, gc.h など




CRuby のソースコードリーディングメモ

以下は、自分が CRuby のソースコードリーディングをした際のメモです。


  • CRuby は大きく以下のステップで処理を進める


    • 1. parse (AST 生成)

    • 2. compile (YARV コード生成)

    • 3. YARV 実行(ランタイム)



  • 1. それぞれのステップは、どこを見たら書かれてる?


    • 1. parse


      • parse.y

      • bison で parse.c を生成


        • lex.c とかに依存(lex.c は defs/keywords から生成)。尚、defs/keywords と同じ内容の def/lex.c.blt など、違うファイルとして同じ内容のものをいくつか見つけたけど意図は不明。。。w




      • yyparse を定義。rb_parser_compile_string_pathrb_parser_compile_file_path を定義


        • ruby.c の load_file_internal から利用される。これは rb_load_file から利用。





    • 2. compile


      • compile.c



    • 3. runtime


      • たくさん!

      • vm.c

      • vm_core.h

      • vm_insnhelper.h

      • vm_insnhelper.c

      • vm_method.c

      • etc…

      • insns.def からコード生成


        • insns.def というファイルには、オレオレDSLで「YARV 命令の定義」が書かれてる!


          • 生成されたファイルは compile.c などから利用。

          • vm_exec.c の中で、vm.inc を利用。vm_exec_core という関数の内部に展開される。



        • tool/insns2vm.rb で実行


          • tool/instruction.rb の中で RubyVM::SourceCodeGenerator などを定義



        • -> vm.inc


          • 実際に compile される形式の命令列

          • vm_exec.h 内でフラグで「使われ方」が決まる。基本的には DIRECT THREADED CODE という形式で使われてる(vm_opts.h を見ると、OPT_DIRECT_THREADED_CODE に 1 が立ってる)

          • INSN_ENTRY マクロを展開してラベル付与。

          • YARV命令に対応するコード実行後、TC_DISPATCH マクロの処理として goto *(void const *)GET_CURRENT_INSN(); を実行。これで、次の YARV 命令のラベルの箇所にジャンプ。これをずっと繰り返す。

          • Threaded code はるびま参照 http://magazine.rubyist.net/?0008-YarvManiacs



        • -> insns.inc


          • template/insns.inc.tmpl が template


          • ruby_vminsn_type という enum を定義(命令1つ1つに対応)



        • -> insns_info.inc


          • template/insns_info.inc.tmpl が template


          • insn_name_info という名前を管理する array や util function などを定義(コード中では insn は int で表してるので、名前が必要になった時などこれを利用。その他バイトコード長や operand info など。)





      • YARV の命令列を見たいときは?



        • RubyVM::InstructionSequence.compile("a = 1 + 2").disasm がサクッと見れてオススメ!

        • 追記: コメントで教えてもらいましたが、ruby --dump=insns -e 'a = 1 + 2' のように --dump=insns というオプションを利用する事で、 irb を立ち上げなくても YARV 命令列が見れるそうです!



      • メソッドディスパッチ



        • sendopt_send_without_block などを見ると良い!



          • vm_search_method というそれっぽい関数を呼んでる





      • その他個別の class や method の定義







  • 2. Ruby コードの実行順序


    • main.c の中で main 関数を定義。以下を順に実行。

    • 1. RUBY_INIT_STACK マクロ

    • 2. ruby_init



      • ruby_setup を呼んで、問題があったらそこで終了


        • stack の初期化、VM の初期化、heap の初期化などをやる。ruby_prog_init という hook point もここにある。





    • 3. ruby_options



      • ruby_process_options を呼び出して引数に応じてソースコードを compile。iseq というYARV命令の内部表現を返す。

      • -> ruby_process_options


        • 状況に応じて、 rb_parser_compile_stringload_file を使い parse して tree (AST) を生成。その後、rb_iseq_new_main で iseq(YARV命令列)を生成。 iseq は rb_iseq_t * 型。typedef struct rb_iseq_struct rb_iseq_t; されてて、 rb_iseq_struct という struct。

        • -> rb_parser_compile_string (parse.c で定義してる関数) -> yycompile -> yyparse

        • -> rb_iseq_new_main (compile.c で定義してる関数) ->rb_iseq_compile_node` (ここで AST から YARV 命令の並びを生成)





    • 4. ruby_run_node



      • ruby_exec_node に iseq を渡し、iseq(YARV命令列) を順番に実行。

      • -> ruby_exec_node



        • ruby_exec_internal に iseq を渡し、iseq(YARV命令列) を順番に実行。

        • -> ruby_exec_internal


          • iseq と rb_thread_t *th を用意して、 rb_iseq_eval_main を呼ぶ



        • -> ruby_exec_internal -> rb_iseq_eval_main



          • iseqvm_set_main_stack に渡して control frame pointer と PC を適切なものに更新。


          • vm_exec で YARV の動作を開始。



        • -> ruby_exec_internal -> rb_iseq_eval_main -> vm_set_main_stack



          • vm_set_eval_stack -> vm_push_frame で、ruby の control frame を追加。th->ec.cfp, th->pc, th->iseq を更新。これで、GET_PC などで返ってくる pc が想定する iseq になる。



        • -> ruby_exec_internal -> rb_iseq_eval_main -> vm_exec



          • vm_exec_core(th, initial) を実行。exception_handler などもここに設置



        • -> ruby_exec_internal -> rb_iseq_eval_main -> vm_exec -> vm_exec_core


          • ループで (*GET_PC())(th, reg_cfp) を実行。ただ、DIRECT THREADED CODE の場合は 命令末尾で jump して命令実行し続けるので、おそらく返ってこない。