*STDRRには不用意にレイヤを追加しないほうがいい

以下のコードは「done.」だけ出力して終了する*1

#!perl -w
use strict;
binmode *STDERR, ':encoding(foo)';
print join(' ', PerlIO::get_layers(*STDERR)), "\n";
END{
	print "done.\n";
}
__END__

これはbinmode()がdieするためだが,本来なら,:encodingに不正なエンコード名を与えても,レイヤの追加に失敗するだけでdieしたりはしないはずだ。また,普通はdieしたとしてもエラーメッセージを表示してから終了するはず。しかしなぜ何も表示せずに異常終了するのか。
それは追加するファイルハンドルが*STDERRだからだ。
binmode()の動作を追ってみる。まず,binmode()ルーチン*2は:encodingレイヤのインスタンスを作成し,*STDERRレイヤスタックにその未初期化の:encodingレイヤインスタンスを追加する。その後:encodingのイニシャライザが呼び出されるが,"foo"というエンコーディングは存在しないため,:encodingのイニシャライザは警告を発しようとする。そこで警告メッセージを*STDERRに出力しようとするが,*STDERRには初期化が終わっていない:encodingレイヤインスタンスが乗ったままなので,:encodingを介した警告メッセージの出力は失敗に終わる*3
以上が沈黙のdieの流れである。*STDERRに警告を発する可能性のあるレイヤを追加するのは注意が必要だ。なお,上記のコードの場合,binmode()をeval{}で補足し,例外が発生した後binmode(*STDERR, ':pop')で初期化の終わっていない:encodingを取り除けば*STDERRの状態は回復する。

*1:今回の問題はPerlIO::Util 0.52-0.53のバグフィクス中に発見した。

*2:正確に言うと,binmode()ルーチンから呼び出されるPerlIO_push()で起こる出来事だ。ちなみにPerlIO::Utilのpush_layer()はこのAPIへのインターフェイスである。

*3:binmode()の例外を補足すると「Can't call method "encode" on an undefined value」となっているが,これは未初期化の:encodingに出力しようとした結果である。SEGVじゃないだけまだマシか。