pipe

rubyコマンドを実行し、pipeを利用して実行結果*1を受け取ろうとしたら、なぜかパーサがブロックしてしまい残念な結果になってしまっていた。もういやになってうっちゃっていたが、このままではTyping Rubyの開発が一歩も進まないので*2、落ち着いて調べてみた。

結論だけ言えば、Unix.create_processは標準出力として与えられたfile_descrをUnix.closeしてくれないので、与えた標準出力の反対から読むようにしているとそこでブロックしてしまうことが原因だった。どうやるのが定石なのかはちょっとわからないが、とりあえずUnix.wait_pidしてUnix.closeしてやる関数を作って別スレッドで待たせてやれば問題は解決する。

まず、単純にpipeとして動作するプログラムを書いてみる。

let simply_pipe () = 
  let args = Array.of_list ["cat"; "foo.ml"] in
  let pid = Unix.create_process "cat" args Unix.stdin Unix.stdout Unix.stderr in
    Unix.waitpid [] pid

let _ = simply_pipe()

このプログラムはうまく動作した。

そこで、barに与えたチャンネルから1行ずつ読んで出力するプログラムを作ってみる。

let like_ruby () =
  let args = Array.of_list ["cat"; "foo.ml"] in
  let (out_in, out) = Unix.pipe() in
  let pid = Unix.create_process "cat" args Unix.stdin out Unix.stderr in
    try
      let inchan = Unix.in_channel_of_descr out_in in
      while true do
        let s = input_line inchan in
	  Printf.printf "# %s\n" s;
	  flush stdout
      done
    with
	End_of_file -> ();
    Unix.wait_pid [] pid
      
let _ = like_ruby()

こんな感じ。これは見事にブロックしてくれる。目論見どおりである。どこでブロックしているのかをデバッガで追うと、let s = input_line inchan inでブロックしていた。そこで、Unix.wait_pid [] pidtryの前に持っていっても症状は同じ。

outがクローズされていないことが原因なのだから、Unix.close outをどこかに入れてやればいいことになる。とりあえずtryの前に入れて、うまく動作することが確認できた。

ここまでくれば後は簡単。別スレッドで待ってファイルを閉じてやる関数を動かせばそれでいいはず。

let wait_and_close (pid,out) = 
  Unix.waitpid [] pid;
  Unix.close out

let like_ruby () =
  let args = Array.of_list ["cat"; "foo.ml"] in
  let (out_in, out) = Unix.pipe() in
  let pid = Unix.create_process "cat" args Unix.stdin out Unix.stderr in
  let _ = Thread.create wait_and_close (pid,out) in
  let inchan = Unix.in_channel_of_descr out_in in
    try
      while true do
	let s = input_line inchan in
	  Printf.printf "# %s\n" s;
	  flush stdout
      done
    with
	End_of_file -> ()
      
let _ = like_ruby()

として問題は解決。Thread周りがちょっと不安だけど、まあ大丈夫でしょう。動いてるし。

*1:ruby構文解析の結果。パーサ手書きはアリエナイ

*2:そんなことはないけど