えぇと、今回 1.9 でなにが起きたのかを私が把握している範囲でまとめてお
きます。

基本的には、IO のバッファリングに stdio を使うことをやめて、自前で行う
ようにしたという話です。また、実装を行った私の方針や変更が終っていない
ことによっていくつかの挙動が変化しています。

そもそもなぜ stdio を使いたくないのかという点については
http://pub.cozmixng.org/~the-rwiki/rw-cgi.rb?cmd=view;name=stdio
にいろいろ理由が載っていますが、解決されたものもあれば解決されていない
ものもあります。

* [ruby-dev:22545] HP-UX では fwrite で EAGAIN が設定されない?

  もし HP-UX のバグが write じゃなくて fwrite にあったとすれば、ad hoc
  ではない形で解決できたはずです。

* [ruby-dev:23181] ungetc を使ってよいかどうか簡単には判定できない

  これは簡単に判定できるようになって、ungetc の失敗を確実に判定できる
  ようになりました。その結果、Windows で scanf のテストが成功するよう
  になったようです。
  http://www.dm4lab.to/~usa/ruby/d/200412a.html#id20041207_P1_1

* [ruby-bugs-ja:432] [ruby-dev:22880] [ruby-talk:93917]
  [ruby-talk:95953] fflush() 内の write が EAGAIN のときにバッファの
  データを捨ててしまう

  解決されており、nonblocking mode が原因でデータが消えることはなくなっ
  たはずです。

* [ruby-dev:24102] [ruby-dev:24137] 読み込みバッファを破棄するときの
  seek の失敗を検出できない

  これは解決されています。seek が失敗した場合は双方向ストリームとして
  の動作に移行します。つまり、tty などを open したときには自動的に双方
  向ストリームとして動作します。

* [ruby-dev:1991] stdio 内の読み込みバッファが空かどうか判定する関数が標準にない

  これも解決されており、おそらく、64bit Solaris でも問題なく動作するは
  ずです。

* [ruby-dev:18396] fseek の前に fflush しないといけない実装がある

  もはこういうわけのわからない stdio の挙動にはつきあわなくてもよくなっ
  たので、試行錯誤してコードを書かずとも問題なく動作するようになりまし
  た。

* [ruby-dev:23346] Solaris でファイルを 256個以上オープンできない

  この制約は依然として残っています。これは、IO オブジェクトにそれぞれ
  対して、ひとつ FILE 構造体を割り当てるためです。実際の所、割り当てた
  FILE 構造体はほとんど使っていないので、それをなくして制約を取り除く
  のは可能なはずです。

* [ruby-dev:25014] trap を安全かつ遅延せずに実行させることができない

  これは直っていませんが、どうにかできそうだという見通しはあります。
  [ruby-dev:25056]

* <URL:http://www.katontech.com/diary/?date=20040821#p01> 2byte 以上
  ungetc できない

  2byte 以上の ungetc は stdio と同様に保証されません。
  任意長の unget をサポートするためには、バッファを長くするために
  realloc しないといけないのですが、安全に realloc できるタイミングが
  わからないというのが理由です。trap がどうにかできればこれもどうにか
  できるかもしれません。

また、いくつか挙動が変化します。

* rb_read_pending など、外部に公開していた I/O まわりの関数のうち、
  FILE* を引数に取る関数がうまく動作しない

  バッファは FILE 構造体とは別の所で行われるようになったため、FILE* か
  らはバッファにアクセスできません。そのため FILE* を受けとってバッファ
  を操作する関数は適切にしないようになり、deprecated となりました。
  (gcc 3.1 以降では使用すると警告を出すようにしました)

  原理的には、FILE* から IO オブジェクトを検索する表を管理すればどうに
  かできるかも知れませんが、それでどのくらい嬉しいのかはわからないため、
  いまのところはそういう対処はしていません。

  なお、ruby に同梱されている中では、curses で rb_read_check(stdin) を
  使っている所が残っています。これをどうするかは、curses が標準入力か
  ら読むときに stdio のバッファを扱っているのかどうかを調べてから考え
  ようと思っています。

* EOF flag を実装していない

  stdio には EOF を既に読んだかどうかを記録している flag があるのです
  が、この flag は実装しませんでした。

  その結果、read(0) が常に "" を返すようになりました。
  (以前は、EOF flag がセットされている時には nil を返していました)

  また、eof? はバッファにデータがなければ必ず read を呼び出すようにな
  りました。以前は EOF flag がセットされている時には呼び出しませんでし
  た。このため、IO の対象が端末の場合には ^D などで EOF を送るまで待つ
  ことが以前よりも増えています。[ruby-dev:25078]
 
  この変更は [ruby-dev:22334] で主張し、[ruby-dev:22343] で受け入れら
  れた EOF flag を除去するという方針を最後まで進めたものです。

* ひとつの IO オブジェクトにはひとつの file descriptor を持つ

  いままでの Ruby は双方向 popen を実現するのにふたつの単方向 pipe を
  使っていました。そのために、ひとつの IO オブジェクトにふたつの file
  descriptor が入っていることがありました。

  しかし、この file descriptor と IO オブジェクトのくいちがいは繁雑で
  あるうえ、file descriptor に対する fcntl などでは適切な動作がなんな
  のか不明瞭なことがありました。

  そのため、今回は socketpair というもの双方向 popen を実装しすること
  によって、ひとつの file descriptor で済まし、ふたつの file
  descriptor を持つ機構を除去しました。

  これの最も大きな利点は、コードが明瞭になったことだと思うのですが、
  それ以外の利点としては [ruby-talk:122649] で述べている双方向パイプに
  対しても fcntl でアクセスモードが得られないという問題が解決するといっ
  たことが思い当たります。

  しかし、これには問題もあって、Windows には socketpair がないため、
  Windows では双方向 popen が動かないようになってしまいました。

  ただ、Windows でも TCP で (無理矢理) socketpair のようなものを実装す
  ることは可能なので、それが実装されたときには双方向 popen も復活する
  のではないかと想像しています。

  なお、Windows で socketpair を実現すると、これはパイプと違って
  select が効くので、timeout が効き、readpartial のテストが刺さるのを
  解決する糸口になるかもしれないなどの可能性があります。

* text mode を考慮していない

  text mode は気にしていないため、text mode に関係する挙動がどうなって
  いるかは不明です。いまのところ具体的にどういうトラブルが起こるのかも
  不明です。

* nonblocking mode における IO#read の挙動の変化

  nonblocking mode で IO#read を使用した場合、EAGAIN に出会っても即座
  には返らず、blocking mode と同様に指定した長さに達するまで読むように
  なりました。これは [ruby-dev:17866] での判断とは異なります。

  私があえてこうしたのは、O_NONBLOCK をより簡単に使うためです。
  O_NONBLOCK は write でプロセス全体がブロックすることを防ぐためには必
  須なのですが、O_NONBLOCK を設定したことによって IO#read の挙動が変わ
  ると、IO#read を使っているプログラムでは O_NONBLOCK を気楽に設定する
  わけにはいきません。しかし、nonblocking mode でも blocking mode と同
  様に動作するのであれば、ブロックしちゃったらとりあえず O_NONBLOCK を
  設定してみる、ということが気楽にできるようになります。

  また、O_NONBLOCK は file descriptor (正確には file table のエントリ)
  を共有しているプロセス間で共有されるため、他のプロセスが勝手に変えて
  しまう可能性があります。そのようなことがあると、Ruby スクリプトが感
  知できない所で IO#read の挙動が変わってしまうため、IO#read がどちら
  の挙動をとるとも期待できなくなり、期待しなかったほうの挙動が起こると、
  プログラムが不適切な動作をするようになってしまいます。この危険性を除
  去するためにも、blocking/nonblocking の違いに依存して挙動を変えるこ
  とはしないようがいいと思います。

  ただし、もともと nonblocking mode の IO#read の仕様は、到着してるデー
  タだけを読みたいという要求から決まったわけで、その要求はそれはそれで
  どうにかしないといけません。しかし、1.9 にはすでに readpartial があ
  り、それで問題なく解決できるはずです。ただ、readpartial は 1.8 に入っ
  ていないので、現在のままだと 1.8 と 1.9 の両方で問題なく動作するプロ
  グラムを書くには場合わけが必要になってしまいます。そこで、1.8 にはそ
  のうち readpartial をいれて、IO#read で挙動が変わるところを利用した
  時 (EAGAIN になった時) には警告を出すようにしたらいいんじゃないかと
  思っています。

また、現在の実装は最終的なものではなく、まだ変化する可能性があります。
具体的には次のことを考えています。

* OpenFile 構造体から f を除去する

  こうすると、FILE 構造体を使わなくなるため、Solaris での 256個制限が
  回避できるはずです。

  ただし、これを行うと、ext/openssl/ossl_bio.c の BIO_new_fp のように
  拡張ライブラリで GetOpenFile を使って FILE* を得ている部分で困るかも
  知れません。まぁ、その部分で fdopen すればいいのではないかという気も
  するのですが、使っているところのメンテナと相談したい所です。

  また、これを行うと popen(3) を使うコードを捨てる必要があります。しか
  し、じつは最近の Ruby ではそこの部分にバグがあって、コンパイルできな
  かったはずです。したがって、捨てても大きな影響はないはずだと考えてい
  ます。
  (popen を使うのは HAVE_FORK も _WIN32 も定義されていない時なのですが、
  そこで fpr という変数を使っていたにもかかわらず、その変数は _WIN32
  が定義されているときしか定義されていませんでした)

* text mode をどうにかする?

  Unix では関係ないのですが、それ以外での text mode をどうにかする必要
  があるかも知れません。

  いまのところどういう問題が生じているのかも不明ですが、今まで stdio
  がやっていたような処理 (というのが何なのかよくわかっていないのですが)
  を行うべきか、あるいは chomp などを使ってスクリプトレベルで処理した
  ほうがいいのか議論が必要でしょう。

* 安全な trap の処理

  まぁ、これはやっていけない理由はないと思うのでとくに議論は必要ないと
  思います。実現されたときには trap がもうちょっと安定して使用できるよ
  うになる、はずです。
-- 
[田中 哲][たなか あきら][Tanaka Akira]