読者にわかるように、仕様をもって処理の概要を説明してしまおう。この関数でしないといけないことは、以下の通りである。1個の文字列を、argv形式、つまり文字列の配列に分割しなければならない。Stringだが、出力は引数で指定されたエンコーディング(ここではUTF-8)の配列になっていなければならない。ワイルドカード文字を含む要素はglobbingして展開しなければならない。引用符の類はそのまま渡ってきているので、適切に処理しなければならない。Windowsユーザーのための処理なのだから、ユーザーの混乱を避けるため、Ruby独自の便利機能以外は、なるべくOS標準のコマンドライン分割処理に準じなければならない。Ruby独自の便利機能とは、独自メタ文字の展開、および、単一引用符(')も引用符として扱うことを指す。Cランタイムのワイルドカード展開処理では二重引用符でくくられた部分にあるメタ文字も容赦なく展開されるが、Ruby独自の便利機能では二重引用符については同様とするが、単一引用符にくくられた場合は展開を抑止するものとする。OS標準のコマンドライン分割処理について公式ドキュメントに付け加えるとすると、二重引用符が連続した場合の扱いだろうか。"""""""""である。実は公式のドキュメントには(見落としでなければ)書かれていないが、3つの連続した二重引用符は二重引用符1文字(引用符の開始/終了ではなく)とみなし、それを踏まえた上で二重引用符が1個だけ余るケースは単に引用符の開始または終了とみなす、という挙動になっているからである。1個の場合は引用符の開始(または終了)、2個の場合は当たり前だが引用符を開いて閉じただけ、となる。最終的に引用符が開始されたけど終了してない場合は、末尾で閉じ忘れただけといなされる。Rubyの場合は、単一引用符もこの挙動の対象とする。あと、OS標準では、ドキュメントにあるように、バックスラッシュ(フォントによっては円記号、\)の直後に二重引用符が来た場合はこの一組で二重引用符という1文字(引用符の開始/終了でなく)として扱い、それ以外の場合にはバックスラッシュはバックスラッシュという文字として扱う、というルールもある。これを演繹して、バックスラッシュの次のメタ文字はglobbingの展開対象せずその文字そのものとして扱う、とかいう風に拡張したくなるのが人の常であるが、Windowsではバックスラッシュはパス区切り文字という重要な役割があるので、さすがにそれは採用できない(例えばC:\*が期待と異なる意味になってしまう)。引用符文字そのものを表す場合(つまりRubyの場合は二重引用符だけでなく単一引用符も対象)にのみバックスラッシュによるエスケープを有効とすることにする。奇妙な仕様として、これもドキュメントにあるように、2n+1個のバックスラッシュの次に二重引用符が来た場合は、n個のバックスラッシュと1個の二重引用符とされる。2n個のバックスラッシュの次に二重引用符が来た場合はn個のバックスラッシュになる(二重引用符はくくりの開始または終了とみなされる)。Rubyでは、単一引用符もこの奇妙な挙動の対象とする。ちなみに、バックスラッシュの後に二重引用符が連続しない場合、バックスラッシュの数が減らされたりはしない。仕様はこれで理解してもらえたと思うので、あとは難しいコードはあまり読まずに、処理の中身を文章でざっくり説明して終わることにする。説明した仕様のとおりに、引用符を意識したコマンドライン分割処理を行う。Stringから指定のエンコーディング(UTF-8コードページでしたね)に変換する。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文字列配列を連続したメモリ領域に配置しなおしたものに置き換えている。うん、コードは難しいんだけど、仕様がわかっていて文章で説明する分には問題ないよね。いやしかし今回は長かった。問題ないと言っておいてなんだが、私たちRubyコア開発者もよくそのことを忘れて痛い目にあっている。エンコーディングライブラリ初期化前にRubyレベルのエンコーディング操作メソッドを呼ぶ処理を入れちゃってRubyインタプリタを動かなくしちゃった、というコミットが時たま発生している。踏むのはたいていWindowsだけなので仕方ない面もあるのだが、普段Windows版を見てるはずのコミッタがやらかしたこともあるので(実は私も一度やった)、皆さん気をつけよう。前回でw32_cmdvector()は終わったので、rb_w32_sysinit()(win32/win32.c)に戻ることにする。残りは淡々とWindows固有初期化処理が続くだけである。tzset()はCランタイム関数で、Unix系OSのtzset(3)と同じである(たぶん)。環境変数TZがあれば、それをアプリ(ここではRubyインタプリタ)のタイムゾーンとして採用する。(標準時)"とかを返すと「ギャッ!」と叫びたくなる体質の人は、環境変数TZをJST-9とか設定しておくとハッピーになれるかもしれない。init_env()は環境変数関連の初期化。Windowsでは普通設定されていないHOME環境変数・USER環境変数・TMPDIR環境変数を用意している。HOMEは、もしあればそのまま採用、なければ、環境変数HOMEDRIVEとHOMEPATHを繋いだもの、環境変数USERPROFILE(いじってなければHOMEDRIVE+HOMEPATHと同じはず)、ユーザーのプロファイルフォルダ(いじってなければUSERPROFILEと同じはず)、ユーザーのパーソナルフォルダ(現代だとマイドキュメントのこと)、のうち最初に見つかったものを採用する。USERは、もしあればそのまま採用、なければ、環境変数USERNAME、システムのログインユーザー名(いじってなければUSERNAMEと同じはず)、の順で見つかったものを採用する。ここで得られた結果はgetlogin()の結果としても使われる。TMPDIRは、もしあればそのまま採用、なければ、環境変数TMP、環境変数TEMP、ローカルアプリケーションデータフォルダの中のtempディレクトリ、システムディレクトリ(普通はC:\Windows)の中のtempディレクトリ、のうち最初に見つかったものを採用する。init_func()はRubyインタプリタ内部で割と頻繁に使いたいんだけどOSのバージョンによってはあるかどうかわからない大事なAPI(具体的には現状はCancelIO()のみ)の存在確認。init_stdhandle()は標準入出力のファイルディスクリプタの初期化。前に述べたように、汎用的なものは後でfill_standard_fds()で行われるが、Windows版Rubyにはrubyw.exeという生まれながらにして標準入出力を持たない版が添付されているので、もし標準入出力がオープンされていないなら、NULデバイスを結び付けている。これをやっておかないと、スクリプトが標準出力に何か出力しようとしたり、警告などが標準エラー出力に出力されたりすると、当然出力先がないので例外が発生するのだが、その例外もどこにも出力できないので、はたから見るとrubyw.exeが黙って突然死したように見えてしまう。結果、ユーザーの皆さんから「なんじゃこりゃゴルァ!」と怒られることになるので、それを避けるためである。atexit()はC標準関数でプログラム終了時に行われる処理を登録する関数。実際に登録しているexit_handler()では何をやっているかというと、Windows版固有のリソースの解放である。最後に、Windowsではソケットを使用する際には事前にソケットライブラリ(WinSock)の初期化処理が必要なので、StartSockets()関数を呼んでその中でそれを行っている。思われるかもしれないが、例えばRubyインタプリタ内部でカジュアルに呼び出されてるselect(2)なんかはWindowsにおいてはWinsock提供機能の一つだとかいうことがあるので、もうソケットは使うものだとみなして最初に初期化しちゃっている。いうわけで、rb_w32_sysinit()はこれでおしまいである。後は、process_options()の中でちょっと触れた、コマンドライン文字列のエンコーディングについて。externalになったりすることについて悩ましいという話をしたが、以上見てもらったとおり、WindowsではいったんUTF-8としてコマンドライン文字列を取得している。externalなりに変換をしている。マクロUTF8_PATHが有効の場合の処理がそれである。1.9.3以前は素のコマンドライン文字列取得の時点でWではないAPI、即ちGetCommandLineA()を利用していた。出力はCP_ACP(ANSIコードページ、概ねlocaleエンコーディングみたいなもの)である。2.0.0以降はGetCommandLineW()を利用するようにはなったが、rb_w32_sysinit()から返されるコマンドライン解釈結果はCP_ACPのままだった。StringからCP_ACP文字列に変換していた。1.9.3以前ではRubyインタプリタ実装に入る前、OSの中でこの変換が行われており、2.0.0ではRubyインタプリタの内部、rb_w32_sysinit()の中でこの変換が行われるようになり、trunkではさらに後のprocess_options()の中で変換されるようになったわけである。Rubyの版が進むにつれて徐々に変換位置が後ろになっていっているのは、Rubyインタプリタでこの変換処理を制御することによって、不具合を取り除き、よりユーザーにとって扱いやすいようにしようという努力の現れである。そもそも、なぜ変換を行うのかというと、互換性のためである。要するに、Windowsでは(というか他のプラットフォームでもそうだが)昔からコマンドライン文字列はlocaleエンコーディングで与えられていたため、スクリプトレベルの互換性を維持するためにはそれを容易には変更できなかったのだ。Windowsの内部では結局全部Unicodeで取り扱っているわけで、いちいちlocaleエンコーディングに変換するのも馬鹿らしい話ではある。3.0だろうか)で、このあたりは全部UTF-8になるようにして、仕様をすっきりさせたいところである。いうわけで、Windows版特殊処理の解説もこれで終了である。長かった本連載も今度こそ本当にこれにておしまいとなる。全17回にもなるとは、書き始めた時には思いもしなかった......。この連載を書いた理由は、「あれ、ひょっとして、人類で現在のRubyのスタートアップコードの中身を理解してるのって私だけ(*2)なんじゃね?」という怖いことを思ってしまったためであった。この連載のおかげであと10人くらいは理解してる人が増えただろう、と思いたい。?では、その10人くらいの皆さん、長々とお付き合いありがとうございました。自スレッドこの一言に深い闇が凝縮されているが、今回は省略。1.8後に行われた数々の変更、特にエンコーディング絡みの処理や、Windowsで何をやってるのかということまで含めると、あながち冗談でもなさそうで......。非人類なら私より詳しいのが一匹存在する。secindex.rdfにてRSSを試験的に提供しています。