Gc.finalise について

OCaml には、 Gc.finalise という関数があります。これはガーベージコレクター(GC)に関連のある関数で、

ある値が必要なくなってガーベージコレクトされる直前に、その値に対して何かするためのコールバックを登録する

ために使われます。

Gc.finalise f v

とすると、v が GC される直前に f v を評価します。

まず、謎

なぜ、finalise であって finali*z*e ではないか、これは OCaml 七ふしぎの一つです*1。オブジェクト初期化コードのための予約語は initiali*z*er なのにね、、、大昔理由を聞いたような気がしますが、、、確か、 finalize という名前の関数が既にコンパイラか何かで使われていたから、とかいう理由だったような、、、

次に、効用

リソース開放のタイミングを自動的に行えます。v がリソースのハンドルとすると、ハンドルが GC されるということは最早、そのハンドルが使われることはない => もうどこでも使われない => 開放しても良いということになるので、ハンドルを開放(close)する関数をコールバックとして登録できます。

注意1: GC されない物がある

そもそも、GC されない値に対してコールバックは登録できません。Unix.file_descr は実は内部では整数として表されています。OCaml では整数は GC の対象になりません(よりマニアックに言えば、unboxed value)なので、Gc.finalise してもエラーになりますよ。

# let f fd = prerr_endline "GC! Closing!"; Unix.close fd in
  let fd = Unix.openfile "hoge" [Unix.O_WRONLY; Unix.O_CREAT]) o666 in
  Gc.finalise f fd
  ;;
Exception: Invalid_argument "Gc.finalise"

こういう値に対して、自動 close を行いたい場合は、box化を行います:

type t = FD of Unix.file_descr
let f (FD fd) = prerr_endline "GC! Closing!"; Unix.close fd
let t = 
  let fd = Unix.openfile "/etc/passwd" [Unix.O_RDONLY]) o666 in
  FD fd
let _ = Gc.finalise f t

こうすれば、t が必要なくなったとき = ファイルハンドル fd が必要なくなったとき、だから、finaliser を使ってクローズできます。その後、まちがっても t 中の fd を使った書き込みは起こりませんから安全です。
ただ、ひとつ気を付けたいのはハンドル fd を t から抜き出してはだめだということ。そんなことをすると、t 自体は GC されちゃったけど、ハンドル fd はまだアクセスできるから、自動クローズした後でもファイルにアクセスしちゃうコードが書けてしまう:

let FD fd = t in
...
(* t がガベコレされて、Unix.close fd が呼ばれる *)
...
Unix.write fd ... (* まだ fd を覚えている!エラー。 *)

これを避けるには型 t の実装を signature (.mli) で隠蔽して、間違っても t から fd を取り出せないようにすればよいです。

注意2: finaliser をまず定義しよう

finalise の対象を定義したり、その値に対して、 Gc.finalise を実際に呼び出す前に、finaliser を定義するようにしてください。gc.mli にも書いてありますが、

let v = ... in Gc.finalise (fun x -> ...) v

と、まず値を定義してから finaliser を定義すると、思った通りに finaliser が呼ばれません。なぜか。Finaliser (fun x -> ...) はクロージャを作りますが、そのクロージャの中に v が含まれています。ということは、finaliser のクロージャから v が到達可能です。v 到達可能である限り、 GC されませんから、finaliser が GC されるようなシチュエーションにならない限り、 v も GC されません。v の finalisation が遅れることになります。さっさと不必要になっていても、最悪の場合、プログラムが終了しても絶対呼ばれない(注意3参照)ということもありえます。これは、まずはともかく、finaliser を定義することに気を付ければ防げます。

注意3: finaliser が呼ばれるとは限らない

OCaml プログラムは、その実行を終了する場合、その時点でメモリ上にあるデータをいちいち GC してから終了する、なんてことはしません。だってそんなことしても時間の無駄ですから。ですから、プログラムの実行終了*近く**2まで使用中の(到達可能な)値に対しては、 finaliser を登録しても呼び出されないことがあります。Gc.finalise を使う場合には、finaliser が呼ばれない可能性もあることを考慮しなくてはいけません。at_exit 関数*3を使わなければいけないかもしれません。

で、次のパラグラフは、書いてみたんだけど、とても簡単な回答を id:osiire さんから頂いてしまいました。情けないですが残しておきます。

じゃ、ここでパズルです。値 v とその finaliser f があります。v に対して GC が起こった場合 f v を呼び出します。プログラム終了時までに v が GC されなかった場合は、at_exit を使ってやはり f v を呼び出したい。そんな関数 finalise_or_at_exit が定義できますか? この finalise_or_at_exit 関数は、場合によっては非常に沢山の値に対して使われるかもしれませんから、不必要なメモリリークは避けなければいけません。例えば、次みたいなのはダメです:

let finalise_or_at_exit f v =
  at_exit (fun () -> f v);
  Gc.finalise f v

もしかしたら、まずなぜこれがダメか考えたらよいかもしれません。

気になる点

Gc.finalise で面白いのは、finaliser の中で到達できなくなって GC の対象となった v が再び到達可能になっても構わない、という仕様です。ということは、 finalise が呼び出されたのに、実際には GC されない値、というのも作ることができます。まあ、ある関数が引数 v を到達可能にするか、どうかなんて判定は非常に難しいので、この仕様で構わないんですけど、なんだか不思議ですね。

         ,. -‐'''''""¨¨¨ヽ
         (.___,,,... -ァァフ|       あ…ありのまま 今 起こった事を話すぜ!
          |i i|    }! }} //|
         |l、{   j} /,,ィ//|   『おれはGc.finaliseの前まではGCしようと思っていたのに
        i|:!ヾ、_ノ/ u {:}//ヘ    finaliserを呼んだら、いつのまにかGCする気がなくなっていた』
        |リ u' }  ,ノ _,!V,ハ |
       /´fト、_{ル{,ィ'eラ , タ人  な… 何を言ってるのか わからねー(わかんねーよ)と思うが
     /'   ヾ|宀| {´,)⌒`/ |<ヽトiゝ       おれも何をされたのかわからなかった
    ,゙  / )ヽ iLレ  u' | | ヾlトハ〉
     |/_/  ハ !ニ⊇ '/:}  V:::::ヽ       頭がどうにかなりそうだった…
    // 二二二7'T'' /u' __ /:::::::/`ヽ
   /'´r -&#8212;一ァ‐゙T´ '"´ /::::/-‐  \    Obj.magic だとか ocamlbuild だとか
   / //   广¨´  /'   /:::::/´ ̄`ヽ ⌒ヽ    そんなチャチなもんじゃあ 断じてねえ
  ノ ' /  ノ:::::`ー-、___/::::://       ヽ  }
_/`丶 /:::::::::::::::::::::::::: ̄`ー-{:::...       イ  もっと恐ろしいものの片鱗を味わったぜ…

さて、この変な性質を何かに利用できるかと思って、考えたのですが、、、特にないみたいです。使えないことは無いけど、他の方法でもできるし、、、

*1:残りの六つはまた今度ゆっくり考えます。

*2:終了の瞬間である必要はありません。

*3:プログラム終了時に呼び出すコールバックを登録する