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 は大きく以下のステップで処理を進める
-
- parse (AST 生成)
-
- compile (YARV コード生成)
-
- YARV 実行(ランタイム)
-
-
- それぞれのステップは、どこを見たら書かれてる?
-
- parse
- parse.y
- bison で parse.c を生成
- lex.c とかに依存(lex.c は defs/keywords から生成)。尚、defs/keywords と同じ内容の def/lex.c.blt など、違うファイルとして同じ内容のものをいくつか見つけたけど意図は不明。。。w
-
yyparseを定義。rb_parser_compile_string_pathやrb_parser_compile_file_pathを定義- ruby.c の
load_file_internalから利用される。これはrb_load_fileから利用。
- ruby.c の
-
- compile
- compile.c
-
- 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などを定義
- tool/instruction.rb の中で
- -> 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 など。)
- insns.def というファイルには、オレオレDSLで「YARV 命令の定義」が書かれてる!
- YARV の命令列を見たいときは?
-
RubyVM::InstructionSequence.compile("a = 1 + 2").disasmがサクッと見れてオススメ! - 追記: コメントで教えてもらいましたが、
ruby --dump=insns -e 'a = 1 + 2'のように--dump=insnsというオプションを利用する事で、 irb を立ち上げなくても YARV 命令列が見れるそうです!
-
- メソッドディスパッチ
-
sendやopt_send_without_blockなどを見ると良い!-
vm_search_methodというそれっぽい関数を呼んでる
-
-
- その他個別の class や method の定義
- これはそれぞれ完全に独立してるので、読みやすいし手を加えやすい
- 自分もここで String Interporation の performance improvement を行なった!
-
- Ruby コードの実行順序
- main.c の中で
main関数を定義。以下を順に実行。 -
- RUBY_INIT_STACK マクロ
-
ruby_init
-
ruby_setupを呼んで、問題があったらそこで終了- stack の初期化、VM の初期化、heap の初期化などをやる。
ruby_prog_initという hook point もここにある。
- stack の初期化、VM の初期化、heap の初期化などをやる。
-
ruby_options
-
ruby_process_optionsを呼び出して引数に応じてソースコードを compile。iseq というYARV命令の内部表現を返す。 - ->
ruby_process_options- 状況に応じて、
rb_parser_compile_stringかload_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 命令の並びを生成)
- 状況に応じて、
-
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を呼ぶ
- iseq と
- ->
ruby_exec_internal->rb_iseq_eval_main-
iseqをvm_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 して命令実行し続けるので、おそらく返ってこない。
- ループで
-