丁稚な日々

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

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

Dec.11,2014 (Thu)

Revision: 1.7 (Dec.11,2014 00:33)

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

_ 今回はprocess_options()を追うのは一端休止して、その中から呼ばれて呼ばれて呼ばれるload_file_internal2()を見る。
実際のコードは引き続きruby.cを参照。

_ 変数scriptはスクリプト名。
今回はこれがNULLであることはないので、if (script)ブロックの中は必ず通る。

_ 変数fは既にオープンされているスクリプトファイル(または標準入力)を指すIOオブジェクトなわけだが、まずはこれのset_encodingメソッドを呼び出して、外部エンコーディングをASCII-8BITとする。
これは要するに実質無変換でスクリプトを読み込みたい、ということである。

_ 変数xflag0でないケースは今回は考えなくていい(*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インタプリタへの動作指定(プラグマ)とみなして処理する、という機能がある。プラグマと呼ぶより、マジコメ(magic comment)と呼んだ方が通りがいいかもしれない。
プラグマのうち、エンコーディングプラグマ、つまりスクリプトエンコーディング指定については、スクリプトの先頭行以外では機能しない、という制限がある。ただし一番先頭の行がshebang行だった場合は、その次の行でもよい。
たった今shebang行を処理しちゃったのだが、すると後続の処理で「あ、この行エンコーディングプラグマっぽいけど先頭行じゃないし前の行(*2)shebangじゃないからただのコメントだわ」とか扱われちゃうかもしれないので、shebang行だったことを示すために#!だけはfの入力バッファに書き戻しておく。うわーややこしい。
ところで、脱線ではあるが、プラグマにはエンコーディングプラグマ以外にもう一つ、warn_indentというものがある。
こちらはエンコーディングプラグマと異なり、スクリプト中の任意の位置で指定可能である。
なお、これらのプラグマの判定処理は、パーサの中にあるので本稿には出てこない。

_ てな感じで#!ruby行の処理が終わった場合、そこで与えられたオプション指定にエンコーディング指定が与えられていた可能性があるので、真のコマンドラインオプションでエンコーディング指定が与えられなかった場合はそれを反映する(つまり優先順位としては#!ruby行のものより真のコマンドラインオプションの方が強い)。

_ なお、先頭行がshebang行じゃなかった場合は、その確認のために読んじゃった文字をfの入力バッファに書き戻しておく。
そもそも確認のために読む文字がなかった場合は……つまりスクリプトなんか与えられてなかったんですね。

_ という感じで、ここまでで#!ruby行がらみの処理は終り。

_ opt->dumpDUMP_BIT(version_v)以外の場合でなければruby_set_script_name()を呼んでrequire_libraries()を呼んでいる。
って、これ-eの場合に見たよね。意味はまったく同じ。

_ 続いて、スクリプトエンコーディングを最終決定する。
コマンドラインオプションで与えられていたら当然それ、そうじゃない場合で標準入力からスクリプトを読んでるならlocaleエンコーディング、どっちでもないならUTF-8(Ruby 1.9.3までならUS-ASCII)、がスクリプトエンコーディングとなる。
……ん、localeエンコーディング? default externalじゃないの? いやlocaleエンコーディングでいいのかなあ、うーん……。やっぱりM17N難しい。

_ fnilの場合、というのはつまりスクリプトなんかなかったんや、という場合だが、その場合は空文字列をrb_parser_compile_string_path()に渡してお茶を濁す。

_ ちゃんとスクリプトがありそうな場合は、set_encodingメソッドを呼んでfにスクリプトエンコーディングを設定した後、rb_parser_compile_file_path()を呼んでスクリプトのパースを行う。
set_encodingメソッドを呼ぶ際は、第2引数(つまりinternalエンコーディング)に"-"を渡し、default internalが設定されていたりした場合でもスクリプト読み込み時にエンコーディング変換が行われちゃったりしないように配慮している。

_ スクリプトの読み込み(とパース)が終わったら、またset_encodingメソッドを呼んで、エンコーディングを設定しなおしている。
設定しているエンコーディングはparserのエンコーディングである。
え、parserのエンコーディングって、そりゃ当然さっき設定したスクリプトエンコーディングだろ? と思われるかもしれないが、実はそうとは限らなくて、スクリプトにBOMがあった場合はBOMのエンコーディングが作用するし、エンコーディングプラグマがあってもそっちが優先される。
あと、さっきinternalエンコーディングを変えちゃったのでそれをリセットする(今はnilになっているが、default internalが設定されているならこの呼び出しでそれになる)という意味も(たぶん)ある。
なお、わざわざエンコーディングを設定する必要があるのは、前回ちらっと言ったように、__END__があった場合はここから戻ったload_file_internal()の中でfDATAに設定する処理が存在するためである。

_ パースの結果得られた構文木を返して、以上でload_file_internal2()は終了。
今回はここまで。

付記

(*1) 今回は考えなくていい
xflagが立つのは、DOS系プラットフォームにおいて、読み込み対象が拡張子.exeを持つ場合のみ。
つまりRubyスクリプトに関する処理ではない。

(*2) 前の行
なぜかパーサで読めない(笑)
いやもちろん読めないのはここで食べられちゃってるため。

Dec.12,2014 (Fri)

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

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

_ load_file_internal2()を読み終わって、これで無事にスクリプトのパースが終わったはずなので、process_options()に戻ることにする。
ソースコードは変わらずruby.cを参照されたい。

_ まずruby_set_script_name()を呼んで(何回目だっけ?)、スクリプト名を設定する。今度は本当にスクリプト名(または"-e"または"-")である。

_ もし--yydebugが指定されていたら、スクリプトのパース処理の途中でノードダンプが表示済みのはずなので、ここでQtrueを返して終了する。

_ 続いて、もしopt->ext.enc.indexが正の値ならそのエンコーディングを、そうでなければlocaleエンコーディングをdefault externalとして設定する。
あれ、これ前にやらなかったっけ? と思うかもしれないが、前回説明したようにスクリプトを読み込むことによってエンコーディングの指定が変化しちゃってる可能性があるので、改めてここで再設定している。

_ default internalも同じ理由でこの時点で再設定する。

_ また、これで今度こそ最終的にdefault external・default internalは確定したので、rb_stdio_set_default_encoding()(実装はio.c)を呼んで標準入出力IOオブジェクトに設定する。

_ ここまでやって、treeNULL……ってことはスクリプトのパースが失敗していた場合、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)の処理。
-pは平たく言うと、

while $_ = gets
  # ここにユーザーのスクリプトが展開される
  print $_
end

という処理を行うオプションである。
この部分では、rb_parser_append_print()(parse.y)を呼んで、まずは既にパース済みの構文木の後ろにprint $_に相当するノードを追加している。

_ 次は-pの続きまたは-n(opt->do_loop)の処理。
-nは先ほどの-pの処理からprint $_を省いたものである。
-pの場合のprint $_の追加は今やったので、-pの場合と-nの場合をまとめてここで処理できる。
rb_parser_while_loop()を呼んでいるが、その際に引数としてopt->do_lineopt->do_splitを渡している。
opt->do_line-lのことで、上記ループ中のgetsのところで自動的に入力内容をchop!する(つまり改行文字を捨てる)という指定である。
opt->do_split-aのことで、$_の中身を自動的にsplitして結果を$Fに格納するという指定である。
rb_parser_while_loop()(parse.y)の中では、opt->do_splitの場合は$F = $_.splitに相当するノードを既存の構文木の前に追加している。
また、opt->do_lineの場合はさらにその前に$_.chop!に相当するノードを追加している。
その上で、構文木全体をwhile $_ = getsでくくるノード(これは専用のノードが存在する)でくるんでいる。

_ opt->do_loopの場合はsubgsubchopchompという4つのRubyグローバル関数を定義している。
これらは$_をレシーバとするStringクラスの同名メソッドだと思ってもらって差し支えない。
つまり-pまたは-nが指定された場合だけ、$_を手軽に扱うためにこれらのグローバル関数が定義されるわけである。
Ruby 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()は終わる、と思う。

付記

(*1) 不要なんじゃないだろうか
パチモンの指摘によると、これはpreludeではなくてBEGIN{}ブロックをよけているらしい。
というわけでこの段落は完全に誤りなのだが、言い訳すると、なのにpreludeとかNODE_PRELUDEとかいう名前が付いてるのが悪い!(逆ギレ)

Dec.13,2014 (Sat)

Revision: 1.2 (Dec.13,2014 00:32)

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

_ 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()は終了である。
な、長かった……。
前にも書いたとおり、このiseqは最終的にruby_run_node()に引数として渡され、Ruby VMによって実行される。

_ 以上で、Rubyのスタートアップ処理を、多少駆け足の部分もあったが、全て追って解説してみたということになる。
みなさんお疲れ様。私が一番疲れたけどね!
たかがスタートアップ処理、されどこの長さと濃さは、なかなか味わい深いものがあったことと思う。
みなさんが普段書いてるRubyスクリプトを、実際にRubyインタプリタが読み込んで実行するまでには、これだけの膨大な処理が行われているのである。
誰もがRubyのクラスライブラリ(オブジェクトシステム)のことばかり気にするし、たまに処理系内部に立ち入る人も、評価器か、せいぜいパーサあたりに目を向けるのが関の山だが、たまにはこのような根っこの部分を眺めてみるのも、また一興だったのではないだろうか。

_ それでは皆さん、また何かの機会にお会いしよう!

_ ……。

_ …………。

_ あれ、何か忘れてないかい?

_ ……そういえば、連載の最初の頃に、「Windows用の特別処理については後のお楽しみ」とか言ってたような……。

_ というわけで、次回からはエクストラステージ、Windows用特別処理。
さらなる圧倒的な闇が読者(と私)を襲う!

Dec.14,2014 (Sun)

Revision: 1.7 (Dec.14,2014 01:49)

[短期集中連載「Rubyスタートアップ」] 第14回 Windows用特別処理(その1)

_ この門をくぐる者は一切の希望を捨てよ。

_ というわけで、Windows用特別処理である。
連載第2回でruby_sysinit()を眺めているときに省略した処理について、ここから見て行くことになる。
Windowsなんてしらねー、とかいう舐めたユーザーも世の中には散見されるようだが、ぜひ一読しておかれることをお勧めする。
なんといっても、Rubyのスタートアップ処理で最も闇が深い(ので、考えようによっては面白い)のはこの部分なのだ。

_ とりあえずruby.cを開いてruby_sysinit()の該当部分に戻ってみると、rb_w32_sysinit()という関数を呼んでいるだけである。
実装はwin32/win32.cにあるのでそちらを見てみよう。
……いきなり#ifプリプロセッサ指令かよ!

_ RUBY_MSVCRT_VERSIONというマクロは使用しているCランタイムのバージョンを示す定数である。
これが80以上、つまりVisual 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ランタイムライブラリのバージョンによって微妙に構造が違う。
違うならそれぞれのバージョンに個別に対応すればいいだけじゃん、と思われるかもしれないが、ファイルディスクリプタ構造体の中身は公開ヘッダには記載されていない。
いや、公開ヘッダにはなくても、Visual C++には参考資料としてCランタイムのソースが添付されているのでそれを参照すればいいのだが……実はCランタイムはサービスパックで更新されることがあって、その更新されたバージョンのソースは提供されない。
そして、ここまで読んだら想像がつくかと思うが、サービスパックでCランタイムが更新されたときにも、ファイルディスクリプタ構造体は変化することがある(*1)のである!

_ なにこの地獄。詰んだ。

_ だが、うろたえるんじゃあないッ! Windows版メンテナはうろたえないッ!
仮にファイルディスクリプタの構造が変化することがあっても、Rubyが操作しないといけないメンバは遥かむかーしのバージョンからある極めて基本的な部分であり、その部分が変化する可能性は極めて低い。
実際、バージョンアップで変化したのは、構造体の後ろの方に新しいメンバが追加されただけであり、Rubyがアクセスしている古くからあるメンバはそのまま構造体の前の方に残されていた。
ということは、ファイルディスクリプタ構造体のサイズが検出できさえすれば、あとはそれを利用して自前で計算してやれば、知りたい情報にはアクセスできることになる。

_ てなわけで、set_pioinfo_extra()関数がそのファイルディスクリプタ構造体のサイズを検出する処理なのである。
具体的には、構造体のサイズを推測しながら公開関数を用いてファイルディスクリプタの中身を変更してやり、狙ったオフセットでその変更結果が検出されれば、推測した構造体のサイズが正しかったのだろう、という手法を用いて構造体のサイズを検出している。

_ ところで、このサイズ検出処理を実行時に行っているのは、CランタイムバイナリはRubyのビルド後に変更されるかもしれないし、またビルド環境と異なるCランタイムがインストールされた環境にRubyバイナリがインストールされる可能性もあるためである。
いやー、闇が深い。

_ で、えーと、RUBY_MSVCRT_VERSION80未満の場合のSetErrorMode()呼び出しというのは、これはWindowsのAPIなのだが、なにか致命的なエラー(普通はメモリ破壊系)が起きたときにOSが出しちゃうメッセージボックスを抑止するための指定である。
うーん、これ、80以上でも有効にすべき気がちらりほらり。

_ get_version()というのは実行中のOSのバージョンを取得する関数である。
なんでOSのバージョンを取得しておく必要があるかというと、OSのバージョンによって処理を変更しないといけない場合があるので……。
実際は可能な限りそのようなケースはバージョンで見るんじゃなくて実際の機能の有無を確認して処理を分けているのだが(UNIX系OSでならconfigure時に検出していることを、Windowsの場合はバイナリ配布が多いだろうと想定して実行時にやってると思ってもらっていい)、どうしても外的な確認では決定できずにOSバージョンで判定しないといけないものもある(例えば、あるAPIで特定の引数の値を受け付けるかどうかがOSバージョンによって違うことがある)ので、そのために取得している。

_ なんかまだちょっと見ただけなのに、結構長くなってしまった。
続きは次回。

付記

(*1) 変化することがある
具体的にはVisual C++ 2005 SP1でそれが起こった。

Dec.15,2014 (Mon)

Revision: 1.1 (Dec.15,2014 00:13)

[短期集中連載「Rubyスタートアップ」] 第15回 Windows用特別処理(その2)

_ さて、次はw32_cmdvector()(win32/win32.c)という関数なのだが、この関数のコードを読んでいく前に、前提知識を一つ共有しておくことにする。
それは「Windowsではシェルはワイルドカードを展開しない」ということである。

_ Windowsのシェルといえばcmd.exeで、Rubyも基本的にはそれを前提としている。
最近だとPowerShellを使う人も増えてきているのかもしれないが(Windowsでコマンドラインインターフェースを使う人は減ることこそあれ増えることはなさそうなのでそんなことないという気もするが)、いずれにせよ、Windowsのシェルはワイルドカードを展開はしない。

_ えー、dirコマンドとかどうなのよ? とかいう人もいたりするわけだが、これはワイルドカード展開した結果がdirコマンドに渡されているのではなくて、dirコマンドが内部でワイルドカードを展開しているのである。
dirコマンドはシェル内部コマンドなので、それって同じことなんじゃねーの、と思う人もいるかと思うが、同じくシェル内部コマンドであるechoコマンドに*あたりを渡してみれば、言ってることの意味が理解してもらえると思う。

_ というわけで、シェルはワイルドカードを展開しない。
内部コマンドですら、定められたコマンドの場合しかワイルドカードを解釈しないのに、外部コマンドに渡す引数でワイルドカードを展開してくれるなど、もちろんあろうはずもない。
そんなわけで、Windowsでは与えられたコマンドライン引数のワイルドカードを展開するとかしないとかいうのは、各コマンド(アプリ)の責務である。

_ なにそれクソい……とか思われちゃうと困っちゃうのだが、こうやって責務を各アプリに押し付けちゃった代わりに、Windowsではちゃんとワイルドカード展開をサポートする機能を提供している。
一つはOSのAPIであるFindFileシリーズAPI。
もう一つはCランタイムの付属品であるsetargv.objシリーズである。

_ FindFileシリーズAPIというのは、引数でワイルドカードを含む文字列を与えたら、それを展開して実際のファイルにマッチした結果を次々と返してくれるAPIである。
つまり、いわゆるglobbing処理をOSがAPIとして提供してくれている。なにそれ便利!

_ setargv.objシリーズというのはもっと便利なもので、コンソールアプリをビルドする際にこのobjファイルをリンクすると、コマンドライン引数に含まれるワイルドカードが自動的に展開された状態でargvに格納されてからmain()関数が呼ばれるようになる、という優れものである。

_ つまり、Windowsでは、アプリ作者側が、自分のアプリに必要な場合だけワイルドカード展開処理をくっつけることができる、という選択の自由を提供してくれているわけである。
なんだよ、Windowsの方が選択の自由があるだけUNIX系OSよりいいじゃん!
Windows大勝利!!

_ ……そんなうまい話で終わるんだったら「闇」とか言うわけないよねー。ははは。

_ FindFileシリーズAPIも、setargv.objシリーズも、理解するワイルドカードメタ文字は*?だけである(後者は中で前者を利用しているので当たり前だが)。
古代人ならこれでも満足するのかもしれないが、[ ]とか{ }とか**とかを知っちゃってる現代人には、ちょっとこれは許しがたいものがあるだろう。
まして、RubyのDir.globはこれらのメタ文字を解釈してglobbingを行ってくれるのに、コマンドライン引数ではそれを理解しないとか言われちゃうと、普通は「ちょ、なにそれ」と思うものであろう。

_ さりとて、独自にglobbing処理を実装するのは大変では……いや、Rubyインタプリタの中に既にそういう高機能globbing処理が搭載されているなら、コマンドライン引数を解釈するときにそれを使えばいいじゃん! おお、ナイスアイデア!

_ と、いうわけで、RubyのWindows特別処理では、RubyのAPIを利用して自前でコマンドライン引数のglobbingをやっちゃおう、ということが行われている。
それがこのw32_cmdvector()という関数なのである。

_ うは、前提を語るだけで長くなっちゃった。
実際のコード読みは次回からとしよう。

Dec.16,2014 (Tue)

Revision: 1.20 (Dec.16,2014 01:57)

[短期集中連載「Rubyスタートアップ」] 第16回 Windows用特別処理(その3)

_ さて、rb_w32_sysinit()からw32_cmdvector()の実際のコード(win32/win32.c)をいよいよ見ていくことにする。

_ w32_cmdvector()を呼び出す際には、引数として、GetCommandLineW()の結果、CP_UTF8rb_utf8_encoding()の結果、を渡している。

_ まずGetCommandLineW()WindowsのAPIだが、コマンドライン引数をWide Character列(Windows用語ではWide String)のポインタとして返す。
Wide Characterとは、簡単に言っちゃえばUnicode、もう少しだけ深く言えばUTF-16LE(UCS-2という説もあり)な「文字」のことである。Cでの型はWCHARで、符号なし16bit整数。
で、その列であるWide StringというのはつまりいわゆるC文字列(型はchar *)のWide Character版であり、型はWCHAR *ということになる。
勘のいい人は気づいたと思うが、GetCommandLineW() APIの戻り値がWCHAR **ではなくてWCHAR *ということは、つまり、ここで取得されるのは要素ごとに分割されたargv形式の配列ではなく、実際にコマンドラインでユーザーが指定した文字列そのものである。
つまり、コマンドラインの分割処理もWindowsでは本来は各アプリの責務なのである。
もちろん、普通はCランタイムのスタートアップコードがargvに分割してくれてはいるし、CommandLineToArgvW()というAPIもあるのでそれを使うこともできるのだが、Rubyでは独自の追加ルールを用いてコマンドラインを分割するので(具体的には単一引用符(')の扱い)、これらは利用しない。

_ 次のCP_UTF8というのは、コマンドライン解析結果として期待されるエンコーディングの指定である。
CP_UTF8の意味はUTF-8コードページなのだが、コードページというのは、えーと、Windowsにおけるエンコーディング情報みたいなもの、ということでいいかな。詳細はMSDNで

_ その次のrb_utf8_encoding()はRubyのUTF-8エンコーディング情報を返す関数だが、これもやっぱり解析結果として期待されるエンコーディングの指定としてw32_cmdvector()に渡している。

_ しかし、なんでUTF-8コードページとRubyのUTF-8エンコーディング情報という2つのエンコーディング情報を渡す必要があるのだろうか?
まず、Rubyインタプリタとしては、コマンドライン解析結果は当然ながらRubyの理解できるエンコーディングの文字列で頂きたい。
しかし、この時点ではエンコーディングライブラリ本体がまだ初期化されていないので、Rubyは元ネタのWide String、つまりUTF-16LEからUTF-8への変換方法を知らない。
Windowsはもちろんその方法を知っている(APIがある)が、WindowsはRubyのエンコーディング情報を理解しない。
というわけで、WindowsにUTF-8に変換させるためにコードページが必要であり、また一方で変換してできた文字列がUTF-8であることをRubyに教えるためにRubyのエンコーディング情報もまた必要となるわけである。

_ あれ、この時点でRubyのエンコーディング情報も存在しないんじゃないの? と思ったあなたは鋭い。
実は全くその通りで、Init_Encoding()Init_String()もまだ呼ばれていないのだからRubyのStringオブジェクトとしてエンコーディングをいじるような機能はまだ使えない。
しかし、例の3つの組み込みエンコーディングの実装自体は(何しろ組み込まれてるんだから)すでに存在する。
なので、StringオブジェクトとしてではなくCレベルの文字列として操作する分には、その文字列のエンコーディングが何か、ということさえわかっていて、StringクラスのAPIを経由したりせずに組み込みエンコーディングの実装に含まれるプリミティブな関数を呼び出したりする範囲内であれば問題ない(*1)のである。

_ ……なんかここまででいつもの長さくらいになっちゃってるのだが、前回はコード読んでないし今回はまだほんの1行を説明しただけなので、心を鬼にして突き進むことにする。

_ それでは、w32_cmdvector()だ。
長いよー。難しいよー。
なので、読者にわかるように、仕様をもって処理の概要を説明してしまおう。

_ この関数でしないといけないことは、以下の通りである。

  1. 1個の文字列を、argv形式、つまり文字列の配列に分割しなければならない。
  2. 入力はWide Stringだが、出力は引数で指定されたエンコーディング(ここではUTF-8)の配列になっていなければならない。なお各要素の型はchar *である。
  3. ワイルドカード文字を含む要素はglobbingして展開しなければならない。
  4. 引用符の類はそのまま渡ってきているので、適切に処理しなければならない。
  5. Windowsユーザーのための処理なのだから、ユーザーの混乱を避けるため、Ruby独自の便利機能以外は、なるべくOS標準のコマンドライン分割処理に準じなければならない。
  6. Ruby独自の便利機能とは、独自メタ文字の展開、および、単一引用符(')も引用符として扱うことを指す。
  7. Cランタイムのワイルドカード展開処理では二重引用符でくくられた部分にあるメタ文字も容赦なく展開されるが、Ruby独自の便利機能では二重引用符については同様とするが、単一引用符にくくられた場合は展開を抑止するものとする。

_ OS標準のコマンドライン分割処理について公式ドキュメントに付け加えるとすると、二重引用符が連続した場合の扱いだろうか。
というわけでクイズ。以下のコマンドライン引数(の一部)は、Cランタイムスタートアップ処理による分割処理後にどのような文字列となるだろうか?

  1. "
  2. ""
  3. """
  4. """"
  5. """""
  6. """"""
  7. """""""
  8. """"""""

_ 正解は、それぞれ、

  1. 空文字列
  2. 空文字列
  3. "
  4. "
  5. "
  6. ""
  7. ""
  8. ""

である。
なんでかって? 実は公式のドキュメントには(見落としでなければ)書かれていないが、3つの連続した二重引用符は二重引用符1文字(引用符の開始/終了ではなく)とみなし、それを踏まえた上で二重引用符が1個だけ余るケースは単に引用符の開始または終了とみなす、という挙動になっているからである。
なお1個の場合は引用符の開始(または終了)、2個の場合は当たり前だが引用符を開いて閉じただけ、となる。
なお最終的に引用符が開始されたけど終了してない場合は、末尾で閉じ忘れただけといなされる。
Rubyの場合は、単一引用符もこの挙動の対象とする。

_ あと、OS標準では、ドキュメントにあるように、バックスラッシュ(フォントによっては円記号、\)の直後に二重引用符が来た場合はこの一組で二重引用符という1文字(引用符の開始/終了でなく)として扱い、それ以外の場合にはバックスラッシュはバックスラッシュという文字として扱う、というルールもある。
これを演繹して、バックスラッシュの次のメタ文字はglobbingの展開対象せずその文字そのものとして扱う、とかいう風に拡張したくなるのが人の常であるが、Windowsではバックスラッシュはパス区切り文字という重要な役割があるので、さすがにそれは採用できない(例えばC:\*が期待と異なる意味になってしまう)。
なので、あくまで引用符文字そのものを表す場合(つまりRubyの場合は二重引用符だけでなく単一引用符も対象)にのみバックスラッシュによるエスケープを有効とすることにする。

_ なお、ちょっと奇妙な仕様として、これもドキュメントにあるように、2n+1個のバックスラッシュの次に二重引用符が来た場合は、n個のバックスラッシュと1個の二重引用符とされる。
2n個のバックスラッシュの次に二重引用符が来た場合はn個のバックスラッシュになる(二重引用符はくくりの開始または終了とみなされる)。
Rubyでは、単一引用符もこの奇妙な挙動の対象とする。
ちなみに、バックスラッシュの後に二重引用符が連続しない場合、バックスラッシュの数が減らされたりはしない。

_ 仕様はこれで理解してもらえたと思うので、あとは難しいコードはあまり読まずに、処理の中身を文章でざっくり説明して終わることにする。

_ まず、説明した仕様のとおりに、引用符を意識したコマンドライン分割処理を行う。

_ 分割処理が終わったら、この時点での分割後の各文字列をWide Stringから指定のエンコーディング(UTF-8コードページでしたね)に変換する。
前に説明したように、RubyはWide StringからUTF-8への変換方法を(この時点では)知らないが、Windowsは当然知ってるので、この変換にはRubyのAPIは使わずにWindowsのAPIを使う。
rb_w32_wstr_to_mbstr()内で使用しているWideCharToMultiByte()それである。

_ こうしてできたそれぞれのC文字列(リストになっている)について、メタ文字が含まれていてかつ単一引用符の中ではない場合、globbingを行う。
そのためにcmdglob()関数を呼んでいるが、この中でバックスラッシュ(\)をスラッシュ(/)に変換している。
Windowsのルールではバックスラッシュはパス区切りだが、これから使うRubyのglobbing機能であるruby_brace_glob_with_enc()はバックスラッシュをエスケープ文字として扱ってしまうためである。
てなわけで、ruby_brace_glob_with_enc()(dir.c)を呼んでglobbing結果を受け取って、C文字列リストに挿入する。
ruby_brace_glob_with_enc()の中身はさすがに説明していくのは辛いし、本稿の範囲を超えると思うので省略。

_ いちおうこれで終りといえば終りなのだが、最後に出来上がったC文字列配列を連続したメモリ領域に配置しなおしたものに置き換えている。整理整頓ですな。

_ うん、コードは難しいんだけど、仕様がわかっていて文章で説明する分には問題ないよね。
いやしかし今回は長かった。
そして次回はようやく最終回!

付記

(*1) 問題ない
と言っておいてなんだが、私たちRubyコア開発者もよくそのことを忘れて痛い目にあっている。
エンコーディングライブラリ初期化前にRubyレベルのエンコーディング操作メソッドを呼ぶ処理を入れちゃってRubyインタプリタを動かなくしちゃった、というコミットが時たま発生している。
踏むのはたいていWindowsだけなので仕方ない面もあるのだが、普段Windows版を見てるはずのコミッタがやらかしたこともあるので(実は私も一度やった)、皆さん気をつけよう。

Dec.17,2014 (Wed)

Revision: 1.5 (Dec.17,2014 01:17)

[短期集中連載「Rubyスタートアップ」] 第17回 Windows用特別処理(その4)

_ 前回でw32_cmdvector()は終わったので、rb_w32_sysinit()(win32/win32.c)に戻ることにする。
残りは淡々とWindows固有初期化処理が続くだけである。

_ tzset()はCランタイム関数で、Unix系OSのtzset(3)と同じである(たぶん)。
環境変数TZがあれば、それをアプリ(ここではRubyインタプリタ)のタイムゾーンとして採用する。
Time.now.zone"東京 (標準時)"とかを返すと「ギャッ!」と叫びたくなる体質の人は、環境変数TZJST-9とか設定しておくとハッピーになれるかもしれない。

_ init_env()は環境変数関連の初期化。
Windowsでは普通設定されていないHOME環境変数・USER環境変数・TMPDIR環境変数を用意している。
HOMEは、もしあればそのまま採用、なければ、環境変数HOMEDRIVEHOMEPATHを繋いだもの、環境変数USERPROFILE(いじってなければHOMEDRIVE+HOMEPATHと同じはず)、ユーザーのプロファイルフォルダ(いじってなければUSERPROFILEと同じはず)、ユーザーのパーソナルフォルダ(現代だとマイドキュメントのこと)、のうち最初に見つかったものを採用する。
USERは、もしあればそのまま採用、なければ、環境変数USERNAME、システムのログインユーザー名(いじってなければUSERNAMEと同じはず)、の順で見つかったものを採用する。
なおここで得られた結果はgetlogin()の結果としても使われる。
TMPDIRは、もしあればそのまま採用、なければ、環境変数TMP、環境変数TEMP、ローカルアプリケーションデータフォルダの中のtempディレクトリ、システムディレクトリ(普通はC:\Windows)の中のtempディレクトリ、のうち最初に見つかったものを採用する。

_ init_func()はRubyインタプリタ内部で割と頻繁に使いたいんだけどOSのバージョンによってはあるかどうかわからない大事なAPI(具体的には現状はCancelIO()のみ)の存在確認。
ちなみにCancelIO()は自スレッド(*1)の指定されたI/O処理を中断するWindows APIである。

_ init_stdhandle()は標準入出力のファイルディスクリプタの初期化。
前に述べたように、汎用的なものは後でfill_standard_fds()で行われるが、Windows版Rubyにはrubyw.exeという生まれながらにして標準入出力を持たない版が添付されているので、もし標準入出力がオープンされていないなら、NULデバイスを結び付けている。
これをやっておかないと、スクリプトが標準出力に何か出力しようとしたり、警告などが標準エラー出力に出力されたりすると、当然出力先がないので例外が発生するのだが、その例外もどこにも出力できないので、はたから見るとrubyw.exeが黙って突然死したように見えてしまう。
結果、ユーザーの皆さんから「なんじゃこりゃゴルァ!」と怒られることになるので、それを避けるためである。

_ atexit()はC標準関数でプログラム終了時に行われる処理を登録する関数。
実際に登録しているexit_handler()では何をやっているかというと、Windows版固有のリソースの解放である。

_ 最後に、Windowsではソケットを使用する際には事前にソケットライブラリ(WinSock)の初期化処理が必要なので、StartSockets()関数を呼んでその中でそれを行っている。
require "socket"しない限りソケットは使えないはずだから、これはsocket拡張ライブラリの初期化処理にやらせればいいんじゃない? と思われるかもしれないが、例えばRubyインタプリタ内部でカジュアルに呼び出されてるselect(2)なんかはWindowsにおいてはWinsock提供機能の一つだとかいうことがあるので、もうソケットは使うものだとみなして最初に初期化しちゃっている。

_ というわけで、rb_w32_sysinit()はこれでおしまいである。

_ 後は、process_options()の中でちょっと触れた、コマンドライン文字列のエンコーディングについて。
これがlocaleエンコーディングになったりdefault externalになったりすることについて悩ましいという話をしたが、以上見てもらったとおり、WindowsではいったんUTF-8としてコマンドライン文字列を取得している。
その上で、process_options()の中でlocaleエンコーディングなりdefault externalなりに変換をしている。
マクロUTF8_PATHが有効の場合の処理がそれである。

_ 昔々、つまりRuby 1.9.3以前は素のコマンドライン文字列取得の時点でWではないAPI、即ちGetCommandLineA()を利用していた。
当然出力はCP_ACP(ANSIコードページ、概ねlocaleエンコーディングみたいなもの)である。
Ruby 2.0.0以降はGetCommandLineW()を利用するようにはなったが、rb_w32_sysinit()から返されるコマンドライン解釈結果はCP_ACPのままだった。つまり、w32_cmdvector()の中でWide StringからCP_ACP文字列に変換していた。
整理すると、Ruby 1.9.3以前ではRubyインタプリタ実装に入る前、OSの中でこの変換が行われており、2.0.0ではRubyインタプリタの内部、rb_w32_sysinit()の中でこの変換が行われるようになり、trunkではさらに後のprocess_options()の中で変換されるようになったわけである。
Rubyの版が進むにつれて徐々に変換位置が後ろになっていっているのは、Rubyインタプリタでこの変換処理を制御することによって、不具合を取り除き、よりユーザーにとって扱いやすいようにしようという努力の現れである。

_ で、そもそも、なぜ変換を行うのかというと、互換性のためである。
要するに、Windowsでは(というか他のプラットフォームでもそうだが)昔からコマンドライン文字列はlocaleエンコーディングで与えられていたため、スクリプトレベルの互換性を維持するためにはそれを容易には変更できなかったのだ。
だが、Windowsの内部では結局全部Unicodeで取り扱っているわけで、いちいちlocaleエンコーディングに変換するのも馬鹿らしい話ではある。
互換性を多少壊してもいいタイミング(次回はRuby 3.0だろうか)で、このあたりは全部UTF-8になるようにして、仕様をすっきりさせたいところである。

_ と、いうわけで、Windows版特殊処理の解説もこれで終了である。
そして、長かった本連載も今度こそ本当にこれにておしまいとなる。
まさか全17回にもなるとは、書き始めた時には思いもしなかった……。

_ この連載を書いた理由は、「あれ、ひょっとして、人類で現在のRubyのスタートアップコードの中身を理解してるのって私だけ(*2)なんじゃね?」という怖いことを思ってしまったためであった。
たぶんこの連載のおかげであと10人くらいは理解してる人が増えただろう、と思いたい。
いや10人くらいは最後まで読んでくれたよね? よね?
では、その10人くらいの皆さん、長々とお付き合いありがとうございました。

付記

(*1) 自スレッド
この一言に深い闇が凝縮されているが、今回は省略。

(*2) 私だけ
さすがにそれはないとも思ったのだが、Ruby 1.8後に行われた数々の変更、特にエンコーディング絡みの処理や、Windowsで何をやってるのかということまで含めると、あながち冗談でもなさそうで……。
なお非人類なら私より詳しいのが一匹存在する。


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