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 して命令実行し続けるので、おそらく返ってこない。
- ループで
-