丁稚な日々Rubyで遊んだ日々の記録。著者視点の私的な記録なので、正確さを求めないように。Rubyと関係ない話題にはその旨注記しているはず。関係無いように見える話題もどこかで関係あるのかもしれません。今回はprocess_options()を追うのは一端休止して、その中から呼ばれて呼ばれて呼ばれるload_file_internal2()を見る。実際のコードは引き続きruby.cを参照。変数scriptはスクリプト名。(script)ブロックの中は必ず通る。変数fは既にオープンされているスクリプトファイル(または標準入力)を指すIOオブジェクトなわけだが、まずはこれのset_encodingメソッドを呼び出して、外部エンコーディングをASCII-8BITとする。これは要するに実質無変換でスクリプトを読み込みたい、ということである。変数xflagが0でないケースは今回は考えなくていい(*1)のでopt->xflagの方だけ説明すると、これはコマンドラインオプション-xが指定されているかどうかを意味し、効果は#!rubyで始まる行(「ruby」の部分はパスを含んでいてもよい。いうか、普通は含んでいると思う)が見つかるまではその部分はRubyスクリプトじゃないとみなして読み捨てる、というものである。-xにはもう一つ、引数としてディレクトリ名が与えられていたらカレントディレクトリをそこに変更する、という機能があるのだが、カレントディレクトリ変更はproc_options()内で-xを見つけたときに既にやっちゃっている。いうわけでopt->xflagが立っていた場合だが、普通スクリプトの行番号は1から始まるが(だからline_startの初期値は1)、今から読み捨て処理をするのでとりあえず開始位置を0とする。forbid_setid()でセキュリティチェック(説明済み)をした後、fから1行ずつ読み込んでshebang行(#!で始まる行)を探す。shebang行が見つかって、それがrubyという文字列を含んでいたら、探していた行だとみなしてラベルstart_readにジャンプする。結局最後まで読んでも探していた行が見つからなかったらLoadError例外を投げて終了である。opt->xflagが立っていなかった場合は素直に先頭からスクリプトを読むのだが、やっぱり1行目がshebang行だったかどうかは判定することになる。1行目がshebang行で、かつそれがrubyという文字列を含んでいなかった場合、このスクリプトは少なくとも冒頭部分はRubyスクリプトではないと推察されるので、きっと-xオプションを指定し忘れたのだろうということで、ラベルsearch_shebangにジャンプして、さっきのopt->xflagの場合の探索処理を行う。ちなみに昔のRuby(具体的には1.8以前)では、shebang行にRubyインタプリタじゃないっぽいものが指定されていたら、そのプログラムを起動して処理させる、という機能があった。「ほらほら、rubyにperlスクリプトを渡してもちゃんと動くよ!」とかいうジョークに使えたのだが、なくなってしまってちょっと寂しい。opt->xflagが立っていて#!ruby行が見つかった場合、または、スクリプトの先頭行が#!ruby行だった場合、ひょっとするとこの行にRubyインタプリタに対するオプション指定があるかもしれない。それが見つかった場合、moreswitches()を呼んで、その内容を変数optに反映する。ところで、Rubyには、特別なコメントがあった場合、それをRubyインタプリタへの動作指定(プラグマ)とみなして処理する、という機能がある。comment)と呼んだ方が通りがいいかもしれない。プラグマのうち、エンコーディングプラグマ、つまりスクリプトエンコーディング指定については、スクリプトの先頭行以外では機能しない、という制限がある。先頭の行がshebang行だった場合は、その次の行でもよい。今shebang行を処理しちゃったのだが、すると後続の処理で「あ、この行エンコーディングプラグマっぽいけど先頭行じゃないし前の行(*2)shebangじゃないからただのコメントだわ」とか扱われちゃうかもしれないので、shebang行だったことを示すために#!だけはfの入力バッファに書き戻しておく。ところで、脱線ではあるが、プラグマにはエンコーディングプラグマ以外にもう一つ、warn_indentというものがある。こちらはエンコーディングプラグマと異なり、スクリプト中の任意の位置で指定可能である。これらのプラグマの判定処理は、パーサの中にあるので本稿には出てこない。感じで#!ruby行の処理が終わった場合、そこで与えられたオプション指定にエンコーディング指定が与えられていた可能性があるので、真のコマンドラインオプションでエンコーディング指定が与えられなかった場合はそれを反映する(つまり優先順位としては#!ruby行のものより真のコマンドラインオプションの方が強い)。先頭行がshebang行じゃなかった場合は、その確認のために読んじゃった文字をfの入力バッファに書き戻しておく。そもそも確認のために読む文字がなかった場合は......つまりスクリプトなんか与えられてなかったんですね。いう感じで、ここまでで#!ruby行がらみの処理は終り。opt->dumpがDUMP_BIT(version_v)以外の場合でなければruby_set_script_name()を呼んでrequire_libraries()を呼んでいる。これ-eの場合に見たよね。続いて、スクリプトエンコーディングを最終決定する。1.9.3までならUS-ASCII)、がスクリプトエンコーディングとなる。いやlocaleエンコーディングでいいのかなあ、うーん......。fがnilの場合、というのはつまりスクリプトなんかなかったんや、という場合だが、その場合は空文字列をrb_parser_compile_string_path()に渡してお茶を濁す。スクリプトがありそうな場合は、set_encodingメソッドを呼んでfにスクリプトエンコーディングを設定した後、rb_parser_compile_file_path()を呼んでスクリプトのパースを行う。internalが設定されていたりした場合でもスクリプト読み込み時にエンコーディング変換が行われちゃったりしないように配慮している。スクリプトの読み込み(とパース)が終わったら、またset_encodingメソッドを呼んで、エンコーディングを設定しなおしている。設定しているエンコーディングはparserのエンコーディングである。思われるかもしれないが、実はそうとは限らなくて、スクリプトにBOMがあった場合はBOMのエンコーディングが作用するし、エンコーディングプラグマがあってもそっちが優先される。internalが設定されているならこの呼び出しでそれになる)という意味も(たぶん)ある。エンコーディングを設定する必要があるのは、前回ちらっと言ったように、__END__があった場合はここから戻ったload_file_internal()の中でfをDATAに設定する処理が存在するためである。パースの結果得られた構文木を返して、以上でload_file_internal2()は終了。今回は考えなくていいxflagが立つのは、DOS系プラットフォームにおいて、読み込み対象が拡張子.exeを持つ場合のみ。Rubyスクリプトに関する処理ではない。前の行なぜかパーサで読めない(笑)いやもちろん読めないのはここで食べられちゃってるため。load_file_internal2()を読み終わって、これで無事にスクリプトのパースが終わったはずなので、process_options()に戻ることにする。ソースコードは変わらずruby.cを参照されたい。ruby_set_script_name()を呼んで(何回目だっけ?)、スクリプト名を設定する。今度は本当にスクリプト名(または"-e"または"-")である。--yydebugが指定されていたら、スクリプトのパース処理の途中でノードダンプが表示済みのはずなので、ここでQtrueを返して終了する。externalとして設定する。思うかもしれないが、前回説明したようにスクリプトを読み込むことによってエンコーディングの指定が変化しちゃってる可能性があるので、改めてここで再設定している。internalも同じ理由でこの時点で再設定する。internalは確定したので、rb_stdio_set_default_encoding()(実装はio.c)を呼んで標準入出力IOオブジェクトに設定する。ここまでやって、treeがNULL......ってことはスクリプトのパースが失敗していた場合、Qfalseを返して終了する。Qfalseの場合は異常終了だったということでRubyインタプリタは終了する(以前説明したruby_executable_node()を参照)。process_sflag()だが、これもやはりスクリプト読み込み中に-sが改めて指定されたかもしれないため。opt->xflagsの初期化は......たぶんまったく意味ない。これもデッドコードかな。最近までここにもセーフレベル4でARGVと$LOAT_PATHをtaintする処理があったのだが、先日のコミットで削除されてしまった。-c(DUMP_BIT(syntax))または--dump=syntaxが指定されていた場合、スクリプトがパースできていればチェックは終了したことになるのでここでQtrueで終了。次は-p(opt->do_print)の処理。$_endという処理を行うオプションである。$_に相当するノードを追加している。次は-pの続きまたは-n(opt->do_loop)の処理。$_を省いたものである。$_の追加は今やったので、-pの場合と-nの場合をまとめてここで処理できる。rb_parser_while_loop()を呼んでいるが、その際に引数としてopt->do_lineとopt->do_splitを渡している。opt->do_lineは-lのことで、上記ループ中のgetsのところで自動的に入力内容をchop!する(つまり改行文字を捨てる)という指定である。opt->do_splitは-aのことで、$_の中身を自動的にsplitして結果を$Fに格納するという指定である。$_.splitに相当するノードを既存の構文木の前に追加している。opt->do_lineの場合はさらにその前に$_.chop!に相当するノードを追加している。getsでくくるノード(これは専用のノードが存在する)でくるんでいる。opt->do_loopの場合はsub・gsub・chop・chompという4つのRubyグローバル関数を定義している。これらは$_をレシーバとするStringクラスの同名メソッドだと思ってもらって差し支えない。-pまたは-nが指定された場合だけ、$_を手軽に扱うためにこれらのグローバル関数が定義されるわけである。1.8までは常時定義されてたんだけどね。ところで、rb_parser_append_print()とrb_parser_while_loop()の中では、preludeが構文木先頭に存在した場合に、それをよけてノードを追加するように配慮がなされているのだが、preludeは現在の構文木中にはなくて既に実行済みなので、この配慮は不要なんじゃないだろうか(*1)。ま、それはともかく、これで-p・-n・-l・-aの場合も含めた構文木の作成が完了した。いうわけで、--dump=parsetree(DUMP_BIT(parsetree))または--dump=parsetree_with_comment(DUMP_BIT(parsetree_with_comment))の場合、ここでrb_parser_dump_tree()(node.c)を呼んで構文木を可読テキストに変換し、標準出力に流して、Qtrueで終了する。次回でprocess_options()は終わる、と思う。不要なんじゃないだろうかパチモンの指摘によると、これはpreludeではなくてBEGIN{}ブロックをよけているらしい。process_options()(ruby.c)の最後はiseq(VMバイトコード列)へのコンパイルである。スクリプト名が"-e"でも"-"でもない場合、ということはつまり実際にファイルからスクリプトが読まれる場合、rb_realpath_internal()を呼んでスクリプトのフルパス名を得る。構文木、スクリプト名、スクリプトのフルパス名を引数としてrb_iseq_new_main()(iseq.c)を呼ぶと、構文木をコンパイルした結果のiseqが得られる。--dump=iseq(DUMP_BIT(iseq)が指定されていた場合、iseqをrb_iseq_disasm()で逆アセンブルした結果のテキストを標準出力に表示し、Qtrueで終了する。場所がここなのかはよくわからないが(もっと前、該当オプションを扱ったところでいいんじゃね?)、-p・-l・-aの各オプションの状態はRubyグローバル変数$-p・$-l・$-aで参照することができるため、それを用意している。忘れちゃいけないセーフレベルの設定(rb_set_safe_level()(safe.c))。最後に、生成されたiseqを返して、process_options()は終了である。長かった......。VMによって実行される。以上で、Rubyのスタートアップ処理を、多少駆け足の部分もあったが、全て追って解説してみたということになる。私が一番疲れたけどね!たかがスタートアップ処理、されどこの長さと濃さは、なかなか味わい深いものがあったことと思う。みなさんが普段書いてるRubyスクリプトを、実際にRubyインタプリタが読み込んで実行するまでには、これだけの膨大な処理が行われているのである。誰もがRubyのクラスライブラリ(オブジェクトシステム)のことばかり気にするし、たまに処理系内部に立ち入る人も、評価器か、せいぜいパーサあたりに目を向けるのが関の山だが、たまにはこのような根っこの部分を眺めてみるのも、また一興だったのではないだろうか。............。......そういえば、連載の最初の頃に、「Windows用の特別処理については後のお楽しみ」とか言ってたような......。いうわけで、次回からはエクストラステージ、Windows用特別処理。この門をくぐる者は一切の希望を捨てよ。いうわけで、Windows用特別処理である。連載第2回でruby_sysinit()を眺めているときに省略した処理について、ここから見て行くことになる。Windowsなんてしらねー、とかいう舐めたユーザーも世の中には散見されるようだが、ぜひ一読しておかれることをお勧めする。なんといっても、Rubyのスタートアップ処理で最も闇が深い(ので、考えようによっては面白い)のはこの部分なのだ。ruby.cを開いてruby_sysinit()の該当部分に戻ってみると、rb_w32_sysinit()という関数を呼んでいるだけである。実装はwin32/win32.cにあるのでそちらを見てみよう。RUBY_MSVCRT_VERSIONというマクロは使用しているCランタイムのバージョンを示す定数である。2005以降のCランタイムの場合、ちと凝った処理をするアプリはランタイムの特別な初期化処理を呼ばないといけないことになっている。具体的には最初の3つの_で始まる関数の呼び出しがそうである。一つ目の_CrtSetReportMode()は、Cランタイムの中で発生するアサーションを黙らせる指定。黙らせていいのか、と疑問に思われるかもしれないが、有効なままだと問題が起きたときにいちいちメッセージボックスが表示される。黙らせても該当の関数はちゃんとエラーを返して静かに終了するので問題はない。このメッセージボックス表示はデバッグ版のCランタイムをリンクしないと表示されないはずなので、実際はこの呼び出しはなくてもいい。ま、おまじないである。二つ目の_set_invalid_parameter_handler()は、一部の関数で変な引数を渡したときに呼び出されるコールバックを設定する関数。具体的にはmktime(3)あたりで範囲外の年月日を渡すと、デフォルトのままだとプログラムが異常終了してしまう。設定しているコールバック関数invalid_parameter()は何もしない関数だが、こうしておけば該当の関数はおとなしくエラーを返す。三つ目の_RTC_SetErrorFunc()はスタックを壊したりバッファオーバランを起こしたりしたときに呼び出されるコールバックを設定する関数。rtc_error_handler()は渡されたエラー内容を標準エラー出力に出力している。今改めて考えたら、これrb_bug()呼び出しにすべき気がしてきた。ここまではCランタイムの関数を呼んでいるだけなのでいいとして、次のset_pioinfo_extra()というのが闇の中でもとっておきの闇の一つである。Windows版Rubyでは自前でファイルディスクリプタテーブルを操作する必要があるのだが(理由については改めて説明するのが面倒なので、このあたりを参照)、困ったことにこのテーブル(というかファイルディスクリプタ構造体)はCランタイムライブラリのバージョンによって微妙に構造が違う。違うならそれぞれのバージョンに個別に対応すればいいだけじゃん、と思われるかもしれないが、ファイルディスクリプタ構造体の中身は公開ヘッダには記載されていない。C++には参考資料としてCランタイムのソースが添付されているのでそれを参照すればいいのだが......実はCランタイムはサービスパックで更新されることがあって、その更新されたバージョンのソースは提供されない。