PerlIO実装メモ#1 - デストラクタ

結論

PerlIOを実装するとき,リソースの開放はPopped()で行う。Close()を実装する必要はない。

解説

実装メモと称していきなりデストラクタの解説から入るのもどうかと思うが,PerlIOの実装にあたって突き当たる3番目の壁*1なので非常に重要である。
さて,Perl v5.10.0時点では,PerlIOレイヤクラスのデストラクタは2種類ある。それがPopped()とClose()だ*2
Popped()は,Perlレベルでは:pop擬似レイヤによって明示的にレイヤを取り除くときに呼ばれるほか,引数なしのbinmode()でも呼び出される可能性があり,また,close()を呼び出したときも呼びだされるメソッドである。このメソッドには,Open()やPushed()などで確保したメモリやファイルディスクリプタなどのリソースを開放することが求められる。
一方Close()はその名の通りclose()の実体となっているメソッドである。しかしclose()が呼ばれた後は必ずPopped()も呼ばれるため,実際にはclose()で行わなければならない処理は存在しない*3
このように,必ず定義しなければならないデストラクタはPopped()だけなのだが,一見するとClose()こそがデストラクタのように思えるため,Close()のみ定義してPopped()は何もしないという誤りを行いがちである*4。そして,そのようなレイヤを指定したファイルハンドルに対してbinmode($fh, ':pop')を行うと,リソースリークが発生してしまう。この仕様*5は非常に分かりにくく,perlio.cの開発者でさえも把握しきれていないようだ。よって実装に当たってはClose()のことを考える必要はなく,vtableを定義するときClose()はNULLでよい。

*1:これは私の場合である。なお,最初の壁は定義しなければならないメソッドが膨大にあってDNBKしたこと,2番目の壁はOpen()の引数が多すぎてDNBKしたことだった。

*2:以後,CからみたPerlIOレイヤクラスのメソッドを言及するときは,名前の最初を大文字にする。

*3:各レイヤが開放しなければならないリソースは,自らが確保したリソースのみであり,他のレイヤのことを考える必要はない。close()が呼び出されたときの処理は「全てのレイヤに対してClose()を呼ぶ」 -> 「全てのレイヤに対して,Popped()を呼んだあとレイヤインスタンスを解放」となっており,Popped()がリソースの開放を一挙に担うのでClose()は何もしなくてよい。

*4::unixと:stdioはまさにこのバグを持っている。この問題は報告済みなので5.10.1および5.8.9では修正されると思われる。

*5:perliol.podでは,Popped()のみ呼ばれてClose()が呼ばれない場合がある旨の記述があるので,この挙動は確かに仕様のようである。