丁稚な日々

Rubyで遊んだ日々の記録。あくまで著者視点の私的な記録なので、正確さを求めないように。
Rubyと関係ない話題にはその旨注記しているはず。なので、一見関係無いように見える話題もどこかで関係あるのかもしれません。または、注記の書き忘れかもしれません...

[直前] [最新] [直後] [Top]

Dec.1,2014 (Mon)

Revision: 1.4 (Dec.02,2014 20:17)

[短期集中連載「Rubyスタートアップ」] 第1回 Rubyのスタートアップ処理の概要

_ Rubyがスクリプトを実際に読み込むまでに必要な処理として、どんなものが考えられるだろうか。

_ すぐに気付くのは、コマンドライン引数の処理である。
当たり前だが、スクリプトはコマンドラインから与えられる(*1)ので、これをやらずしてスクリプトの読み込みはできない。

_ 次に思いつくのは、オブジェクトシステムの立ち上げだろう。
しかしこれはちょっと面白い話題なので(クラスとオブジェクトの鶏と卵問題とか)、たぶん「Rubyのしくみ Ruby Under a Microscope」でも詳細に取り扱っているものと予想される(*2)
よって本稿では詳しくは取り扱わない。

_ 後は……特にない、のかな。
ほんと?
いやオブジェクトシステム立ち上げ以外にも、雑多な初期化処理が存在しているような気はする。
でも、「初期化処理」という言葉だけでは具体的にどんなことをやってるのかはちょっと想像はしにくい。

_ というわけで、実際のコードにもぐってみよう。
RubyはCで書かれているが、皆さんご存知の通り、Cで書かれたプログラムは原則として(*3)main()関数から処理が始まる。Rubyも同様である。
Rubyのmain()関数はmain.cに書かれているので、各自、お手元のRubyソースコードを眺めていただきたい。
手元にソースがない人はあまりお呼びじゃないんだけど、その場合は上記のGitHubへのリンクを参照されたい。
本稿執筆時に参照しているソースと読者の参照しているソースは中身が変わってしまっている可能性があるが、短期的にも変わっちゃうことが予想される部分については本稿中でなるべくそのことに触れるつもりではある。
それでもあんまり大きい食い違いが発生していて「?」となった場合は、各自本稿の日付時点(今回の場合は2014年12月1日)のソースを参照するなどしてほしい。

_ さて、そんなわけでmain()関数だが……短い。
実質的にはruby_sysinit()RUBY_INIT_STACKruby_init()ruby_options()ruby_run_node()が実行されているだけである。
名前を見ればだいたい予想はつくと思うが、最初の3つは初期化系処理、ruby_options()はコマンドライン引数解釈、ruby_run_node()は評価器の実行となる。
ruby_run_node()は本稿の範囲外なので省くが(これの解説も「Rubyのしくみ Ruby Under a Microscope」にたぶん載ってる(*4)だろう)、次回以降では、これ以外の各処理の中身を順次見て行くことにする。

付記

(*1) スクリプトはコマンドラインから与えられる
スクリプトが与えられなかった場合は標準入力から読み込むことになるが、スクリプトが与えられなかったことを知るためにはコマンドライン引数を処理し終わる必要があるので、結局言いたいことに変わりはない。

(*2) 詳細に取り扱っているものと予想される
12月2日追記: 取り扱ってなかった。てへぺろ。

(*3) 原則として
その原則から外れる例として、例えばWindowsのGUIアプリケーションがあって、これはWinMain()という関数から処理が始まることになっている。
たまたまなのだが、実はRubyにもWindows GUIアプリケーションのソースが添付されている(!)。
興味がある人はwin32/winmain.cを参照のこと。
……あぁっ、石を投げないでっ! 騙そうとしたわけじゃないのっ!

(*4) たぶん載ってる
12月2日追記: 関数名は出てこないが、第3章・第4章あたり。

Dec.2,2014 (Tue)

Revision: 1.5 (Dec.02,2014 02:58)

[短期集中連載「Rubyスタートアップ」] 第2回 ruby_sysinitとRUBY_INIT_STACK

_ それでは、ruby_sysinit()を見てみよう。実装はruby.cの末尾にある。
あ、行番号を指定しない(リンクも該当行へははっていない)のは将来の版で行番号が変わるかもしれないからなので、以後もそのつもりで各自該当行を検索してほしい。
で、中身は見ての通り、Windows以外ではグローバル変数にargcargvなどを保存して、fill_standard_fds()を呼んでいるだけである。

_ Windows用の特別処理については、これは後のお楽しみということで取っておこう。今回は省略。

_ fill_standard_fds()の実装はすぐ上にあるはずだが、何をしている関数かというと、ファイルディスクリプタの0,1,2番、つまり標準入出力について、もしこれらが開かれていない場合、ダミーのパイプを作って埋めてしまう、という処理である。
なんでこんなことをしてるかというと、標準入出力はRubyの内部でもいろいろ特別な扱いがなされるのだが、たまたま作ったファイルディスクリプタが0〜2番になってしまって誤ってこの特別扱いの対象となってしまうことを事前に避けているわけである。
あなたがファイルディスクリプタを操作可能な言語処理系を作る機会があるなら、ちょっと覚えておくといいかもしれないテクニックではある。

_ 次のRUBY_INIT_STACKは全大文字なのでマクロであることはすぐにわかると思うが、定義はinclude/ruby/ruby.hの終り頃にある。
で、実体は、つまりruby_init_stack()を呼んでいるくらいである。

_ ruby_init_stack()thread_pthread.cthread_win32.cにあるが、後者、つまりWindows用の実装は空なので説明しなくていい! ツイてる!
前者(pthread版、つまりUNIX系OS用)はなにやら複雑そうなことをしているが……要するにスタックの大きさと下端アドレスを検出して記憶しようとしているという処理である。
GC時にスタックを走査するための事前情報取得、と思ってもらって差し支えない。
ruby_init_stack()はこの後も何度も何度もいろんなところで呼ばれるのだが、初回のGCが起き得るタイミングの前にまた呼ばれるのだとしたら、ここで呼んでるのは実は無意味かもしれない。
というか、次回に見るコードで実はそうなってることが判明するんだけど、その辺どうなのさko1?(私の理解不足ならごめん)

_ ところで、さっき、Windows用の実装は空なのでラッキー、と一瞬思ったわけだが、よく考えたらGCに使う情報がWindowsでは取れないことになるので大変まずいのでは……。
というわけで、念のため、Windowsではどうなっているのか確認しておこう。ちっ、やっぱり何らかの説明は要るんじゃん!
native_thread_init_stack()(thread_win32.c)はスレッド生成時(メインスレッドの場合は、次回以降に出てくる初期化処理時)に必ず呼ばれる関数である。
ご覧のとおり、ここでスタックの大きさと下端アドレスを取得・記憶している。
Windowsの場合はここで確実な値を設定できるので、いちいち気になったとき(?)に確認する必要がないわけである。

Dec.3,2014 (Wed)

Revision: 1.12 (Dec.03,2014 10:05)

[短期集中連載「Rubyスタートアップ」] 第3回 ruby_init

_ ruby_init()eval.cの最初の方にあるが、実質的にはruby_setup()を呼んでいるだけである。
ruby_setup()はそのすぐ上にあるが、なんかいろいろinit(Init)のついた関数を呼んでいて、いかにも初期化処理らしい趣がある。
この辺、いちいち各関数の中身を見ていくときりがないので、ざっくりと説明していくことにする。
個々の関数はそんなに長くないし、難しいこともまだしていないので、興味があれば各自で確認してほしい。

_ まず最初は前回も述べたruby_init_stack()。ほらやっぱりすぐ呼ばれちゃった。中身は説明済みなので略。

_ 次はInit_BareVM()。Ruby VM(よく巷間YARVって言われるアレ)の初期化である。
bareなので、ここではVMのガワを用意してメインスレッドをそれに関連付けてメインスレッドの情報を設定して、くらいのことまでしかしない。
なお、最後に呼んでるruby_thread_init_stack()thread.cにあって、やってるのはnative_thread_init_stack()を呼んでるだけである。
UNIX系OSの場合、その実装はthread_pthread.cにあるが、今回は対象がメインスレッドなので、ruby_init_stack()で検出済みの値を反映してるだけ。
Windowsの場合は前回説明した通りで、ここでメインスレッドのスタックのアドレス・サイズが取得される。

_ Init_heap()はヒープの初期化。特に言うことなし。

_ Init_vm_objects()はVM用の情報格納領域の確保。

_ Init_frozen_strings()は、Ruby 2.2からの機能改善でfreezeされた文字列絡みで性能向上が測られているのだが(これについては2.2リリース時に誰かが解説を書くと思う)、それに利用するためのハッシュテーブルの準備である。なので2.1以前のRubyにはない。

_ だいたいこんなもんでVMの初期化は概ね完了である。

_ PUSH_TAG()EXEC_TAG()POP_TAG()はRuby評価器のソースコードを読んだことがある人にはおなじみだと思うが、エラーハンドリングやループ制御などのための機構なので、ここではその仕組みについては細かく説明はしない(*1)
本稿で必要な範囲だけ説明しておくと、まずPUSH_TAG()POP_TAG()は処理の流れには影響しないので、存在しないものとして読み飛ばしてかまわない。
EXEC_TAG()の戻り値が0の場合の処理がまず実行されて、もしその途中でエラー(ハンドルされない例外を含む)やジャンプ(ループ内のbreakとか)が起きたりしたらその時点で現在の処理は打ち切られ、0以外の場合の処理に移る(ruby_setup()での使用例だと0の場合の処理しかないので、このif文のブロックが打ち切られるだけだが、本稿で将来見ていく処理の中にはelseの場合とかswitchで分岐する場合が出てくるので、そのつもりで覚えておいてほしい)。
ま、要するに、本稿の範囲内で読むコードに関しては、正常系ならEXEC_TAG()の中の処理だけが実行される、ということを覚えておけば問題ない。

_ というわけでruby_call_inits()だが、これはinits.cを実際に眺めてもらおう。
うひゃー、なんかずらずら並んでる。
これがRubyのオブジェクトシステムと、一部のちょっと特別なもの(具体的にはVM関連の残りとか)の初期化処理呼び出しである。
踏み込んでいくと、この順序になってる意味とか、オブジェクトシステム初期化のやり方とか、これがなかなか面白いんだけど、前に書いたように本稿の範囲外なので今回は踏み込まない。興味があれば個別に読んでみるとよい。
ちなみに、実際に呼ばれる個別の関数は、CALL(Method)とあればInit_Method()になる、というふうに、CALLマクロの引数の前にInit_を付けた名前の関数になる。

_ ruby_prog_init()は基本的なRubyグローバル変数の定義である。ruby.cにあるが、見ればわかるので説明は略。
……あ、ぱっと見ただけだとわからなさそうな点が一つあった。
最後にRuby定数ARGVを定義しているが、その実体はグローバル変数rb_argv、と見せかけて、実はこれはマクロ(定義はinclude/ruby/intern.h)で、その実体はrb_get_argv()関数の呼び出しである。
rb_get_argv()の実装はio.cにあるのだが、返すのはARGF.argvである。
そんなもん作る処理あったっけ? と思われたかもしれないが、実はさっき軽く飛ばしたruby_call_inits()の中から呼ばれるInit_IO()の中で、空のRuby配列として生成済みである。
つまり、ARGVARGF.argvは後で誰かがARGVを再定義しない限りは同じ配列を指すことになる。

_ 最後に、これにてVM起動! というわけで、VMの実行中フラグを立てている。

_ と、いうわけで、ruby_init()が終わった時点でRubyのオブジェクトシステムは立ち上がり、VMも用意され、Rubyプログラムを実行する最低限の準備は終わったということになる。

付記

(*1) 細かく説明はしない
興味があれば「Rubyソースコード完全解説」の第13章で詳細に説明がなされている(現在のRubyではマクロの内部実装は異なるが、動作の意味は同じである)ので参照されたい。書籍が手元にない人はWeb版をどうぞ。

Dec.4,2014 (Thu)

Revision: 1.4 (Dec.04,2014 00:30)

[短期集中連載「Rubyスタートアップ」] 第4回 ruby_options

_ 前回まででRubyプログラムを実行する下準備は終わった。
あとはコマンドライン引数を解釈して、渡されたスクリプトを実行するだけである。
「だけである」と言いつつ、ここからが長いんだけどね。

_ それでは、コマンドライン引数解釈を担うはずのruby_options()を見てみよう。
実装はeval.cにあるが、実際の中身はというと、例のPUSH_TAG()POP_TAG()で囲った中でruby_process_options()を呼んでるだけである。
いや他にもあるけど、エラー処理なので気にしなくていい。
そういえば前回言い忘れたが、PUSH_TAG()POP_TAG()に囲まれた範囲内でEXEC_TAG()の戻り値が0の場合は、Ruby例外を送出することができる。
というか、言い換えると、PUSH_TAG()POP_TAG()に囲まれてなければRuby例外を送出することはできない([BUG]になる)。
もちろん、Ruby例外送出可能条件としては、Exceptionクラスが準備済みでなければならないという制約もある(この初期化は例のruby_init()内で呼ばれるInit_Exception()で行われる)。
とりあえず、ruby_process_options()の中ではRuby例外が発生してもいいわけで、割と安心してRubyの各種APIを使用することができる。
まあ、ぶっちゃけ、Rubyの各種APIをうかつには使えないのは前回取り扱った範囲までだった、ということになるわけだが。

_ さて、ruby_process_options()ruby.cにある。これがコマンドライン引数解釈の本体である。
今回この後出てくる関数も全部ruby.cの中にあるので随時参照のこと。

_ まずruby_script()argv[0]を渡して呼び出している。
ruby_script()というのはRubyの$PROGRAM_NAMEを設定する関数である。
しかし、この時点ではまだスクリプトは読み込んでいない、というか、何がスクリプトなのかすら判明していないので、とりあえず仮の名前としてRubyインタプリタ自体を設定している。後でスクリプト名が判明してから置き換えるので、今はこれで大丈夫。
というか、この情報がエラー情報表示とかにも使われうるのだが、スクリプトのロード前に「お前のせいでエラーが起きた」とか言われても困るしね。

_ rb_argv0はこれは本当にargv[0]、つまりRubyインタプリタの名前を保存するためのグローバル変数なわけだが、ネタ元であるグローバル変数rb_prognameruby_script()で設定されたスクリプト名で、ついさっき述べたとおりこの時点ではargv[0]が入ってるはずなのでこれでOK。なんか迂遠だけど。
このrb_argv0はStringオブジェクトなので、ちゃんとGC用に登録しておく。Cのグローバル変数に格納したRubyオブジェクトはこれを忘れるとGCで消えることがあるので注意しよう。
ところで、このrb_argv0、せっかくStringオブジェクトにまでして保管しているのだが、実はスクリプトレベルで参照する方法はない。
ずいぶん昔から、何らかの形でアクセスできるべきだという議論が持ち上がっては消えているのだが、argv[0]は親プロセスが好きに指定できるのでどんな文字列が入ってるかわかんないからうかつに使えないよとか、適切なインターフェース(名前とか)が思いつかないよ、などという理由で採用されないままである。
そのうち消されちゃうかも。

_ コマンドライン引数を処理する際、Rubyに与えられていたオプションスイッチの類は、その解釈結果を一端バッファに格納し、随時そのバッファを参照するという形で利用される。
そのバッファがここで登場する変数optである。
まずcmdline_options_init()で初期値を設定しておいて、それをargcargvと共にprocess_options()に渡している。
解釈結果はprocess_options()の中で消化してしまうので、ruby_process_options()の中ではoptに対する操作は特には存在しない。

_ で、process_options()でめでたくコマンドライン引数解釈が終われば、あとは可能ならsetproctitle(2)を呼んでOSに表示用プロセス名を伝えて(ここも人によっては面白いかもしれないが、私にはあんまり関係ないので省略)、生成されたであろうiseq(VMバイトコード列)を返しておしまい、である。

_ では、次からはいよいよprocess_options()の中身を見ていこう。
ここからが本番だ! 覚悟はいいか?

Dec.5,2014 (Fri)

Revision: 1.3 (Dec.05,2014 01:10)

[短期集中連載「Rubyスタートアップ」] 第5回 process_options(その1)

_ process_options()はstatic関数なので、引き続きruby.cを見ていくことになる。
今まで出てきた関数はほとんどが短かったが、これは長い。今数えたら260行ほどある。
しかしここが今回の連載のメインなので、この関数については何回かに分けて、がんばって読んでいくことにする。

_ 既に述べたように、process_options()はコマンドライン引数解釈を行う関数であるわけだが、ここでいう「解釈」とは何か。
この場合は、字句解析をして、解析した結果に応じた必要な処理を実行する、というくらいの意味である。
なので、まずは字句解析しないといけないのだが、それはこの関数の中にはない。
ぼんやりコードを眺めていると見落としてしまいそうなのだが、関数冒頭部の変数宣言の並び中で、しれっとproc_options()という関数が呼び出されている。これが字句解析を司る関数である。
ということは、変数宣言が終わって残りの250行くらいは、全部「必要な処理を実行」の部分である。

_ それにしても、proc_options()って親関数と似たような名前で紛らわしい。
parse_optionsとかに名前を変えたほうがいいんじゃないかしら?
ま、そんな感想はともかくとして、proc_options()を見てみよう。
……ざっと400行くらいだろうか。
なるほど前回ラストで覚悟が問われたわけだ。

_ およそコマンドラインからオプションを受け取るプログラムでは、いつもその字句解析は面倒極まりないものではあるのだが、Rubyの場合もご他聞に漏れずそうである。
その結果がこのありさまだよ!
しかし、さっきは「がんばって読む」とか言っちゃったものの、本稿は軽い感じの解説を目指しているので、読者の皆さんにまで額に皺を寄せてこのコードを読み解くよう強要するのは本意ではない。私はやむなく読んだけど。
というわけで、さくっと処理の中身の説明だけを述べよう。

_ proc_options()では、前回用意したバッファである変数opt(この関数には引数として渡されてきている)に、コマンドラインオプションを解析した結果をフラグやら文字列変数やらなんやらとして格納している。
なお、オプション-Iの内容はoptには格納されず、この時点で即座に$LOAD_PATHに反映される。

_ よし、400行が2行で終わった。皆さん優しい解説者に出会えてラッキーだったね!
省略しすぎて足りない気もするけど、それはおいおい必要になったらそのときに説明しよう。

_ proc_options()の戻り値は、既に消化し終わったコマンドライン引数の次の位置を指す数値である。
proc_options()で消化したのはオプションの類なので、次に来る引数は、もし-eオプションがなければスクリプト名、あったらその-eで示されたコード、に渡される引数、ということになる。

_ proc_options()が終わってきりがいいので、ちょっと短いけど今回はここまで。

Dec.6,2014 (Sat)

Revision: 1.7 (Dec.07,2014 12:57)

[短期集中連載「Rubyスタートアップ」] 第6回 process_options(その2)

_ ではprocess_options()に戻って処理を見ていこう。

_ まず、前回説明したproc_options()の戻り値分、argcを減らしてargvを進める。

_ 次に、もし-h(DUMP_BIT(usage))か--help(DUMP_BIT(help))が指定されていたらusage()を呼んでヘルプを表示した後終了する。
ちなみに、-hの場合は1画面(通常は85x25であることを想定)に収まる短縮版が表示されて、--helpの場合はそれよりちょっと長い版を表示することになっている。usage()を呼ぶ際にDUMP_BIT(help)を渡しているのはその切り替えフラグである。
あ、そうそう、process_options()はスクリプトをパース〜コンパイルした結果のiseq(VMバイトコード列)を返すことが想定されていて、それが結局ruby_process_options()の戻り値となってruby_options()の戻り値となって、評価器実行関数ruby_run_node()の引数となるのだが、ruby_run_node()はiseqでなくQtrueを渡された場合は何もせずにRubyインタプリタを通常終了させる。
その辺の実際のコードを確認したい場合はeval.cを参照。ruby_run_node()のすぐ上のruby_executable_node()がその判定を行う処理である。
そんなわけなので、以降も含めて、このままRubyインタプリタを終わらせたいケースではこの関数はQtrueを返して終了している。

_ 続いて、--disable-rubyopt(DISABLE_BIT(rubyopt))が指定されていなければ、環境変数RUBYOPTを解釈して、これも変数optに反映する。
具体的にはRUBYOPTの中身を関数moreswitches()に渡して呼び出しているのだが、moreswitches()はその中でproc_options()をまた呼んでいて、今度はコマンドライン引数でなくRUBYOPTの内容を解析する。
この際に同一のバッファoptを渡しているので、当然に先のコマンドライン引数解析の結果はRUBYOPT解析の結果によって上書きされてしまうのだが、-E-K-Uのエンコーディング関連オプションはRUBYOPTよりコマンドライン引数を優先するというルールなので、解析結果のエンコーディング指定についてはいったん退避して後で書き戻している。
なお、src.encがスクリプトエンコーディング、ext.encがexternalエンコーディング、intern.encがinternalエンコーディングである。

_ ところで、-KはRuby 1.8以前との互換性のために残されているオプションなので、指定された場合は警告を表示することになっている。
あれ、-K使ってるけど警告なんか見たことないよ? という人は-wを指定してみること。
optの中には-K指定の有無を示すフラグは存在しないのだが、現在のRubyでコマンドラインオプションでスクリプトエンコーディングを指定する方法は-Kしかないので、スクリプトエンコーディングが指定されていれば-Kが指定されたと理解して、rb_warning()を呼んで警告を表示する。
ちなみに、rb_warning()-wの指定がある場合などのいわゆるVERBOSEモードの時のみ警告を表示する関数で、常に表示したい警告の場合はrb_warn()を使う。

_ --version(DUMP_BIT(version))か-v(DUMP_BIT(version_v))が指定されていたら、お馴染みのバージョン表示を行う。
-vの場合はスクリプト指定があればそれを実行するが、--versionの場合はスクリプト指定が存在したとしてもここでインタプリタを終了する。

_ --copyright(DUMP_BIT(copyright))が指定されていた場合は著作権表示を行う。
しかし、ここではreturn Qtrueしてないけど、--copyrightって確か著作権表示したらインタプリタ終了するんじゃ?
実は、version.cにあるruby_show_copyright()の実装を見てもらうとわかるのだが、この関数は中でexit(3)を呼んでるので、二度と戻ってはこないのだ。
ちょっとわかりにくい、というか、そっちでexit(3)を呼ぶよりこっちでreturn Qtrueするほうがいいような気がするが、あまり使われそうにはないとはいえruby_show_copyright()は公開APIである(この名前ではRuby 1.4の頃から提供されている)。互換性のために、今更ruby_show_copyright()の挙動は変えられない、ということだろうか。

_ そして次は$SAFE >= 4ならARGV(rb_argv)と$LOAD_PATH(GET_VM()->load_path)をtaintしている、のだけど、Ruby 2.1以降はセーフレベルは3までとなったので、これはデッドコードである……とか本稿に書いちゃったら削除されちゃうんだろうなあ(*1)
もしあなたが見たときになくなってたら、Ruby 2.0.0あたりの該当コードを参照するように。

_ ここまでで、コマンドラインオプションのうち、比較的簡単なものについて処理が終わったことになる。
次からは複雑な部分に入っていく。

付記

(*1) 削除されちゃうんだろうなあ
当然というか、12月6日の明け方までにr48724で削除されてしまった。

Dec.7,2014 (Sun)

Revision: 1.5 (Dec.07,2014 12:56)

[短期集中連載「Rubyスタートアップ」] 第7回 process_options(その3)

_ process_options()の続きである。

_ 以前説明したように、既にargvargvはずらしてあるので、-eオプション(opt->e_script)がなかった場合は現在のargv[0]にはスクリプト名が入っているはずである。
もし-vオプションが指定されていてかつもうargc0であれば、つまりスクリプト名が指定されていなければ、ここで処理終了となる。

_ -vオプションがないのにスクリプト名が指定されていなければ、スクリプトは標準入力から与えることになる。この場合、スクリプト名(opt->script)は"-"となる。
argc0じゃないのにargv[0]NULLというケースは普通にやったら存在しちゃいけないと思うのだが(しかし親プロセスが意地悪ならありえる)、とにかくそんな場合や、argv[0]が空文字列の場合は、やっぱりスクリプトは標準入力から読むことになる。
argv[0]が空文字列の場合ということは、つまりruby ""と実行すれば、標準入力からスクリプトが読み込まれるということである。これは標準入力からスクリプトを読ませた上でそのスクリプトに引数を渡したい場合に使えるテクニックなので(""以降に引数を並べればよい)、覚えておくと役立つ機会があるかもしれない……って、普通の人は標準入力からスクリプトを読ませる機会自体がないか。

_ 一方、スクリプト名がちゃんと与えられていても、これで読み込むべきファイルが単純に決定されるわけではない。
あまり使われていないと思うが、コマンドラインオプション-Sが指定されていた場合(opt->do_search)、スクリプト指定がパスを含まなければまず環境変数RUBYPATH、続いて環境変数PATHからそのスクリプトを探す、という仕様がある。
dln_find_file_r()は指定されたパスから指定されたファイルを探す関数で、dln_find.cに実装がある。
この中身を追っていくと長くて複雑なので詳細は割愛するが、引数で渡されたファイル名が絶対パス指定であればそれを、相対パス指定であれば指定されたパス群の中からそのファイルを探して見つかったものを返す(見つからなければNULLを返す)。
-Sが指定されていない場合、または指定されていたけれども環境変数RUBYPATHPATHからそれが見つからなかった場合は、結局現在のargv[0]がそのままスクリプト名として採用される。
最後にargcargvを調整して、スクリプト名として採用された部分を食べてしまう。ということはつまり、これで-eの場合と同じくargv[0]以降はスクリプトへの引数となる。

_ 以上のようにして決定されたスクリプト名はStringオブジェクト化しておき、またもしDOS系環境(Windowsとか)だとパス区切りとして\が使われたかもしれないので/に正規化しておく。

_ Ruby 2.1以降なら、ここでruby_gc_set_params()を呼び出す。
GCチューニングパラメータ関係の環境変数読み込み処理である。
引数でセーフレベルを渡しているのは、セーフレベルが1以上なら環境変数からの読み込みを行わないため。

_ 続いてruby_init_loadpath_safe()を呼び出して、$LOAD_PATHを設定する。
-Iで指定した中身は既に$LOAD_PATHに格納済みなので、ここで追加されるのは標準ロードパスと言われるものである。
ruby_init_loadpath_safe()ruby.c内にあるので見てもらってもよいが、地味に長めで#ifdefだらけで何やってるのかわかりにくいのでこれまたざっくりと中身を説明してしまうと、Rubyのビルド時に決定済みのロードパス(グローバル変数ruby_initial_load_pathsに入っている)か、またはもし可能ならRubyインタプリタ自体からの相対位置で決定されるデフォルトのロードパスか、を$LOAD_PATHに設定している。
ここでも引数でセーフレベルを渡しているのは、セーフレベルが0の場合は環境変数RUBYLIBの内容を$LOAD_PATHに含めるためである。

_ ちょっと短めな上にコードの見た目上は切りが悪いように思えるかもしれないが、次から別の処理が始まるので今回はここまで。

Dec.8,2014 (Mon)

Revision: 1.5 (Dec.08,2014 01:31)

[短期集中連載「Rubyスタートアップ」] 第8回 process_options(その4)

_ process_options()(ruby.c)はまだまだ続く。

_ 前回で$LOAD_PATHが設定されたのだが、ということは、これ以降は非組み込みライブラリの読み込みが可能になった、ということになる。

_ というわけで、さっそく、Init_enc()(dmyenc.c)を呼んで、絶対必須の非組み込みライブラリであるエンコーディングライブラリの初期化を行う。
具体的には、enc/encdb.soという各エンコーディング情報の定義を行う拡張ライブラリと、enc/transdb.soというエンコーディング変換情報の定義を行う拡張ライブラリをロードしている。
Encodingクラス自体の初期化はずっと前にruby_init()の奥で行っているのだが(該当処理はInit_Encoding())、その時点では実はクラスと組み込みエンコーディング(ASCII-8BIT・UTF-8・US-ASCIIの3つ)情報が用意されただけだった。
このInit_enc()呼び出しによってようやくその他の多彩なエンコーディングが使用可能になるわけである。

_ ところで、trunkからはもうなくなってしまったが、Ruby 2.1や2.0.0では、その次にrb_enc_find_index("encdb")という処理があった。
さらに、Ruby 1.9.3だと、rb_enc_find_index("encdb")はあるが前述のInit_enc()呼び出しがない。
これはどういうことかというと、実はRuby 2.1までは、Init_enc()はエンコーディングライブラリをスタティックリンクした場合のみ中身があり、それ以外の場合は何もしない関数だった(trunkでも、エンコーディングライブラリをスタティックリンクした場合は中身がそれ用に差し替えられる)。
もっとも、Rubyを普通にビルドした場合はエンコーディングライブラリはダイナミックローディングされるようになるので、普通にはInit_enc()の中身があるバージョンが使われることはない(スタティックリンクしたい場合はconfigure時に--with-static-linked-ext=yes(*1)(全ての拡張ライブラリをスタティックリンク)または--with-static-linked-ext=enc(*2)(エンコーディングライブラリだけをスタティックリンク)を指定する)。なので本稿ではこのケースのコードについては解説しない。
Ruby 1.9.3以前はそもそもエンコーディングライブラリのスタティックリンク自体がサポートされていなかった。Init_enc()が存在しないのはそのためである。

_ ではrb_enc_find_index()(encoding.c)とはなんじゃらほいというと、引数で渡された名前のエンコーディングを探してそのエンコーディング情報のインデックス値を返す関数である。
しかし、当たり前だが"encdb"などという名前のエンコーディングは存在しない。
だが、rb_enc_find_index()は引数としてまだRubyの知らないエンコーディング名を指定すると、その名前のenc/*.soファイル(つまり普通は各エンコーディングモジュールの実体)を探してrequireしようとするという動作をする。
よって、rb_enc_find_index("encdb")とすればenc/encdb.soがロードされ、前述のtrunkの場合のInit_enc()相当の処理となるわけである。
しかし、rb_enc_find_index("encdb")がエンコーディングライブラリ初期化処理だというのはあまりにもわかりにくすぎる。trunkでまともなコードに変更されて本当によかった。

_ さて、無事にエンコーディングライブラリが初期化されたので、とりあえずlocaleエンコーディングを取ってきて、いつぞや決定したrb_progname(スクリプトレベルでは$PROGRAM_NAME。この時点では初出時に説明した通りRubyインタプリタ名が入っている)のエンコーディングとして設定する(rb_enc_associate())。
エンコーディングを設定し終わったら、もうこのStringオブジェクトはいじられることはないのでfreeze。

_ 続いてrb_parser_new()でパーサを生成する(parse.y)。
この処理自体は単に構造体を確保して初期値を入れてるだけなので詳細は略。
パーサを生成した後は、-yまたは--yydebugが指定されていたら(DUMP_BIT(yydebug))、その旨をパーサに設定する(rb_parser_set_yydebug())。
--yydebugはパーサのデバッグ出力を行うオプションで、実際にその出力処理を行うコードはparse.yの中に……ないんだな、これが。
というのは、この機能はyacc(Rubyの場合はbisonだが)が自動的に生成するものなのだ。rb_parser_set_yydebug()はそれを有効にしているだけである。

_ しかし、パーサ周りはなんでこのタイミングなのだろうね。
ちょっと前後の処理の流れから浮いてるので、もっと後、実際にパーサを使うところの直前の方がいい気がするのだが。

_ ま、とにかく、パーサを生成した後はエンコーディング関連の処理に戻る。

_ この時点ではコマンドラインで-Eなどでエンコーディングが指定されていても、そのエンコーディング名を覚えているだけだった。
ここまで来ればエンコーディング名に対応するエンコーディング情報が取れるようになっているので、default external、default internal、スクリプトエンコーディングのそれぞれについて、opt_enc_index()を呼んで、まずはそれぞれのエンコーディング情報のインデックス値を取得していく。
opt_enc_index()の実装はprocess_options()のちょっと上の方にあるが、先ほど説明したrb_enc_find_index()を呼んでいる。
なお、もしopt_enc_index()に誤ったエンコーディング名を指定した場合は、ここでRuntimeError例外が発生する。

_ エンコーディング情報のインデックス値が求まったところで、今度は実際のエンコーディング情報(rb_encoding構造体へのポインタ)を取得し、default external・default internalとして設定していく。
default externalはrb_enc_set_default_external()で設定される。コマンドラインで指定がない場合、default externalはlocaleエンコーディングとなる。
default internalは指定がなければ何もしないが(内部的にはnil)、指定があればrb_enc_set_default_internal()で設定される。ここでopt->intern.enc.index-1に戻している理由は次回以降に。

_ そして、スクリプト名(opt->script_name)にlocaleエンコーディングを設定してfreezeする。
しかし、うーん、これlocaleエンコーディングでいいんだろうか……。
これはちょっと難しい問題なので、次回でもう少し突っ込んで考えることにする。

_ また、$LOAD_PATH(GET_VM()->load_path)の各要素にもlocaleエンコーディングを設定していく。
しかし、ここでrb_str_dup()するのとfreezeしないのとはなんでだったっけ……?
freezeしないのは以前やろうとしたら「ぎゃっ」という人がいたのでしないという仕様で確定した、んだったようなかすかな記憶がなくもないけど、わざわざ複製する理由はちょっとわからない。
元のStringオブジェクトは他のところで使ったりもしてないと思うし(前回触れたruby_init_loadpath_safe()を参照)、そもそもエンコーディング不定のものを残しておいても意味ないしなあ。
たぶんこいつら最初のGCで真っ先に死ぬよね?
……まあ、いいか。とか書いておいたらまた変更されちゃったりして。

_ というわけで、以上でエンコーディング関連処理はいったん終りである。

付記

(*1) --with-static-linked-ext=yes
Windowsの場合は=yesは不要、というか指定不可。

(*2) --with-static-linked-ext=enc
Windowsの場合はこの指定はできない。

Dec.9,2014 (Tue)

Revision: 1.10 (Dec.09,2014 05:04)

[短期集中連載「Rubyスタートアップ」] 第9回 process_options(その5)

_ 読む人がどんどん減っていて某少年漫画誌ならそろそろ打ち切りとなりそうだが、この誰得連載もprocess_options()(ruby.c)もまだまだまだまだ続く。

_ Init_ext()呼び出しによって拡張ライブラリが初期化される。
といっても、Init_ext()がやるのは拡張ライブラリ全般の初期化ではなくて、組み込み拡張ライブラリの初期化である。
Rubyのビルドシステムに詳しくない人にとっては「はにゃ?」と思われたかもしれないが、Rubyの拡張ライブラリは、標準添付のものに関しては、動的ロードとするだけではなく、設定次第でRuby本体に組み込みとすることができる。
具体的には、前回もちらっと述べたようにconfigure時に--with-static-linked=yesを指定するか(この場合は全拡張ライブラリが組み込みになる)、そこでyesの代わりにスタティックリンクしたい拡張ライブラリ名を,区切りで列挙するか、ext/Setupファイルを編集して組み込みとしたいライブラリ名先頭の#を削除するか、である。
特にその手の指定をしなければ、この関数の中身は空である(dmyext.cを参照)。
中身がある場合も、対象の拡張ライブラリのInit_*が並ぶだけだが。

_ 次に、--disable-gemsが指定されていなければ(DISABLE_BIT(gems))、Gemモジュールを定義する。
これを定義しておくと、次のruby_init_prelude()の中で呼び出されるInit_prelude()で、rubygemsライブラリが自動的にロードされるようになる。

_ preludeというのは、Rubyインタプリタに最初から組み込まれていてRubyインタプリタ起動時に(つまり今から)実行されるRubyコードのことである。
具体的には、prelude.rbgem_prelude.rbenc/prelude.rbの各ファイルの中身が、template/prelude.c.tmplによってprelude.cというソースに加工されて組み込まれる。
prelude.cはRubyのリポジトリには含まれていないが、ビルドの中間生成物として作られるか、もしくはリリースのtarballに同梱されているので確認されたい。

_ Init_prelude()はprelude.cの中にあるので、それを見てもらえばわかるが、組み込まれたRubyコードはprelude_eval()という関数を経由して、VMバイトコード列へのコンパイル〜実行が行われる。
つまり、Rubyインタプリタを起動して最初に実行されるRubyコードはこれらというわけである。
つい先ほど触れたrubygemsロード処理はgem_prelude.rbに書いてあるので興味があれば見てみるとよい。1行だけだし。

_ preludeの実行が終わると、マクロUTF8_PATHが非0ならスクリプト名がUTF-8で与えられているとみなしてlocaleエンコーディングに変換する処理が呼ばれる。
なおこのマクロが非0になる環境はとりあえずWindowsである。
このUTF8_PATHマクロ関連のコードはRuby 2.1以前にはないのだが、その辺は後で機会があれば(というか気力が残っていたら)説明しよう。

_ 続いて、ruby_set_argv()でRuby定数ARGVの中身が設定される。
この関数では、まだ残っているargvの中身をStringオブジェクト化してfreezeしながらrb_argv(以前説明したように、ARGVに相当する配列を返すマクロ)が示すRuby配列の中に格納している。
C文字列からStringオブジェクト化の際、external_str_new_cstr()を使っているが、ここではUTF8_PATH環境の場合は元の文字列がUTF-8だったとみなしてdefault externalに変換し、それ以外の環境であればrb_external_str_new_cstr()を呼んでdefault externalなStringオブジェクトを生成している。

_ さて、前回も軽く疑問を提示したが、同じくコマンドライン文字列から生成されたrb_argv0やスクリプト名、$LOAD_PATHの中身のうち-I経由のものなどがlocaleエンコーディングになるのに、ARGVの中身だけdefault externalなのはなんかズレが生じてよくない気がしないでもない。いやまあ、多くの場合はlocaleエンコーディングとdefault externalは一致するのではあるが。
しかしじゃあどっちに統一するかというとまた難しくて、あるプラットフォーム(UNIX系のことだが)では比較的簡単に外部からlocaleを指定することができるが、別のプラットフォーム(Windowsのこと)ではけっこう難しかったりする。
なら-Eで簡単に指定できるdefault externalがいいかというと、実際にコマンドラインに入力されている文字列のエンコーディングは-Eと関係なくlocaleなのでは、とも言えるわけで……。
というわけで、本来はコマンドライン文字列から生成されたStringオブジェクトは全てlocaleエンコーディングになるのが「正しい」ようには思えるのだが、間違いなくそのスクリプト自体によって利用されると予想される(されないなら指定される意味がない)ARGVに関しては、手軽に-Eでエンコーディングを指定できるようにしておく、というのは、利便性とのバランスを考えると、適当な妥協点なのかもしれない。
と、まあ、この辺の扱いがRubyのM17Nのきちんと整理し切れてない闇の部分の一つなので、将来的にはきっときちんと仕様化されて整理されるのだろうとは思う(*1)

_ その次にはprocess_sflag()なる関数が呼ばれるが、これはsflagというものを設定する関数である。
なにそれ? という読者も多いのではないかと思うが、Rubyのあまり知られていない機能として、Rubyに対して-sというオプションを指定すると、スクリプトへの引数のうち'-'で始まるものについては自動的に同名のグローバル変数に格納してくれる、というものがある(その場合、ARGVからは取り除かれる)。
なおスクリプトが-eで渡されたものの場合はそのままでは機能しないので、試してみて「ならねえよ」とか暴れないように。
この場合は"--"というRubyによるコマンドラインオプション解析を打ち切る指定以降の引数について有効になる(具体的には例えばruby -s -e "p $foo" -- -foo=barのような感じ)。
ま、とにかく、process_sflag()は、そのグローバル変数格納とARGVからの取り除きを行う関数である。

_ 次はいよいよスクリプトの読み込み処理周りである。
ただし、スクリプトのパースやコンパイルは当初から言っているように本稿では扱わないので、本当に読み込むところとその周りだけだけどね。

付記

(*1) 思う
まるで他人事のように言ってるけど、各オブジェクトのエンコーディング初期値を決定した時の主犯の一人は私だったような気もする。

Dec.10,2014 (Wed)

Revision: 1.6 (Dec.10,2014 01:12)

[短期集中連載「Rubyスタートアップ」] 第10回 process_options(その6)

_ もう連載10回目かあ。この連載にもprocess_options()(ruby.c)にもみんな飽きちゃったよねえ。
でも続くのだ。

_ というわけで、今回は予告どおりスクリプト読み込みの周りの処理となる。

_ まずTOPLEVEL_BINDINGの取得。
いつぞやすっ飛ばしたruby_call_inits()の中のどっか(具体的にはInit_VM()(vm.cにて)ですでにTOPLEVEL_BINDINGは出来上がっているので、それを取得しているだけ。

_ いきなりこんなところで定義されてるマクロPREPARE_PARSE_MAINの内容は実際に使うところで説明する。

_ まずは-eオプションでスクリプトがコマンドラインから与えられていた場合。

_ 最初にそのスクリプト文字列にエンコーディングを設定する。
スクリプトエンコーディングが指定されていた場合、というのはつまり-Kオプションがあった場合だが、その場合はそれ、そうでなければlocaleエンコーディングとなる。

_ 次に、opt->dumpDUMP_BIT(version_v)以外の場合でなければruby_set_script_name()を呼んでrequire_libraries()を呼んでいる。
条件の意味がよくわからないだろうと思うので説明すると、opt->dumpというのは、なんか表示はするけど実際のスクリプトの実行はたぶんしませんよ、というケースで立つフラグである。
具体的には-y(--yydebug)、-c--copyright-h--help-v--version--dump=...が指定されたケースのことを意味するのだが、このうち--copyright-h--help--versionだったケースでは、これまで見てきたように既にこの関数を終了してしまっている。
また、--dump=...のうち、...部分がcopyrightusageversionだった場合はそれぞれに対応する独立オプションの場合(--dump=usage-hに対応)と同様にすでに終了済みである。
残るのは-yまたは--yydebugまたは--dump=yydebug-cまたは--dump=syntax-v--dump=...のその他のもの、だが、このうち-vはご存知の通りバージョン表示を行った後スクリプト実行も行うという(説明済み)、opt->dump的には特殊なパターンである。
というわけなので、!(opt->dump & ~DUMP_BIT(version_v))という条件は「スクリプトを実行する場合」という意味になる。

_ ruby_set_script_name()以前見たruby_script()と同じことをする関数で、違いは、ruby_script()の引数がC文字列であるのに対し、ruby_set_script_name()の場合はStringオブジェクトを引数として受け取る、ということくらいである。
opt->script_nameにはこのパスを通っている場合は"-e"が設定されている(その処理はproc_options()の中にある)。

_ require_libraries()は言うまでもなくライブラリのrequire処理で、引数opt->reqlist-rで指定されたライブラリの配列である。
実際の処理は、配列の中から一つずつ取り出してrequireを呼び出しているのだが、ライブラリ名であるStringオブジェクトにdefault externalをエンコーディングとして設定したり、proc_options()で前処理(すぐ上のadd_module()がそれ)の際にいったん隠しオブジェクト化していたのでそれを陽のあたるStringオブジェクトに戻したり、といったことなどもしている。
……あれ、default externalなの? localeエンコーディングでなく? まあいいけど。

_ ここまで終わったらまたruby_set_script_name()を呼んで、スクリプト名を元に、つまりRubyインタプリタ名に戻している。

_ そしていよいよパース処理の呼び出しである。
マクロPREPARE_PARSE_MAINをこの場で脳内展開しながら処理を追うと、まず現在のコンテクストをTOPLEVEL_BINDINGのものであると設定して環境を得る。
次に現在のRubyスレッド(当たり前だがメインスレッド)のparse_in_evalなるメンバを減らす。
これは何かというと、この値が0より大きいなら今はeval中だよ、ということを示すものである。
そして環境ブロックをこのコンテクストのものに設定したら、rb_parser_compile_string()を呼び出して、めでたく構文木が得られたはずである。
呼び終わったらparse_in_evalと環境ブロックを元に戻す。

_ これで-eの場合のパースは終了である。

_ 一方、-eじゃなかった場合。

_ 標準入力からスクリプトを読む場合(スクリプト名が"-"の場合)はセキュリティチェックforbid_setid()を行う。
setuidされてたりsetgidされてたりセーフレベルが1以上だったりした場合は標準入力からは読んじゃだめですよ、ということである。

_ 後はPREPARE_PARSE_MAINの中でload_file()を呼ぶ。
load_file()は、終わったら$.(現在の読み込み行番号)を元に戻す、という処理でラップしながらload_file_internal()を呼ぶ関数である。

_ load_file_internal()は、前処理(今回通るパスでは、スクリプトファイルをオープンする、あるいは標準入力とする、くらい)をしてload_file_internal2()を呼んで後処理をする(__END__があった場合のDATAの用意とか)関数である。
なお、スクリプトファイルをオープンする代わりに標準入力とする条件はスクリプトファイル名が"-"の場合だが、ということは、皆さんご承知かもしれないがruby -とすれば標準入力からの読み込みが行われるということになる。
に「""をスクリプト名に指定すれば」とか言ったが、普通は"-"(*1)だよね。ははは。

_ load_file_internal2()は……長いから次回にしよう。

付記

(*1) 普通は"-"
某パチモンにもつっこまれた。


被捕捉アンテナ類
[Ant] [Antenna-Julia] [Rabbit's Antenna] [Ruby hotlinks]