前回はFFIのごく基本的な使い方について説明しました。実際のプログラミングでFFIを用いる場合には,もう少し突っ込んだ知識が必要になります。「Haskell内部の環境と,FFIを使って呼び出されるべき外部の環境との違い」,これをどのように解決するかを考えなければなりません。
こうした違いには「リソースの管理」「スレッド」「例外の扱い」などがあります。今回はリソースの管理を取り上げて説明しましょう。
GCにリソースの管理を任せる
通常のHaskellプログラムでは,ガーベジ・コレクション(GC)によって使用されなくなったメモリーが適宜解放されることが保証されています。このため,確保されたメモリーが解放されないまま残ってしまうことはありません。
では,FFIを使って呼び出す外部関数が扱うメモリーについてはどうでしょうか?
外部関数が使うメモリーをどのように扱うのが適切なのかは,個々のケースで異なります。例えばCOMオブジェクトのように,同一のメモリーを参照カウント(reference counting)を使って共有している場合,GCによって勝手にメモリーが解放されては困ります。この場合,メモリーを解放する代わりに参照カウントを減らす専用の解放関数を使用する必要があります。同様のことは,他の特殊な方法でメモリーを保持するライブラリにも言えます。
したがって,Cのポインタの参照先にあるメモリーを扱う場合に一番妥当な選択は「何もしないこと」になります。HaskellのGC自体は何もせず,ユーザーがプログラムの中で明示的にポインタを解放すれば,誤ったメモリーの解放によって外部関数が正常に呼び出せなくなるという事態は防げます。
しかし,ポインタをいちいち明示的に解放するのは面倒です。どうすればいいでしょうか。
問題は,GCがポインタの解放を決まった方法で行っている点にあります。なので,GCが行うべき適切な解放処理を,プログラマがポインタに関連付けられればよいことになります。
HaskellのFFIでは,この目的にためにForeign.ForeignPtrモジュールでForeignPtrという専用の型を用意しています。これを使うことで,「ポインタ」「GC」「GCを実行する際に行うべき後処理」の三つを関連付けられます。
ForeignPtr型はnewForeignPtr*を使ってPtr型から作成します。
Prelude Foreign.ForeignPtr> :t newForeignPtr newForeignPtr :: FinalizerPtr a -> GHC.Ptr.Ptr a -> IO (ForeignPtr a) Prelude Foreign.ForeignPtr> :t newForeignPtrEnv newForeignPtrEnv :: FinalizerEnvPtr env a -> GHC.Ptr.Ptr env -> GHC.Ptr.Ptr a -> IO (ForeignPtr a)
newForeignPtrは,ForeignPtrの作成時に,FinalizerPtrをGC時の後処理として渡します。同様に,newForeignPtrEnvは,FinalizerEnvPtrを後処理として渡します。FinalizerPtrとFinalizerEnvPtrは,いずれも「ForeignPtrに格納されたポインタを解放する関数」へのポインタの型です。違いは,FinalizerEnvPtrのほうは,関数ポインタを追加する際の引数として環境へのポインタを取ることです。
Prelude Foreign.ForeignPtr> :i FinalizerPtr type FinalizerPtr a = GHC.Ptr.FunPtr (GHC.Ptr.Ptr a -> IO ()) -- Defined in GHC.ForeignPtr Prelude Foreign.ForeignPtr> :i FinalizerEnvPtr type FinalizerEnvPtr env a = GHC.Ptr.FunPtr (GHC.Ptr.Ptr env -> GHC.Ptr.Ptr a -> IO ()) -- Defined in Foreign.ForeignPtr
FinalizerPtrとFinalizerEnvPtrは,あくまで関数ポインタであって,IOアクションや関数自体ではないことに注意してください。mallocなどを使って確保したメモリーは,freeを使って直接解放することはできません。wrapperなどを使用して関数ポインタ化するか,Foreign.Marshal.Allocモジュールで用意されているfinalizerFreeを使って解放する必要があります。
Prelude Foreign.Marshal.Alloc> :t free free :: GHC.Ptr.Ptr a -> IO () Prelude Foreign.Marshal.Alloc> :t finalizerFree finalizerFree :: GHC.ForeignPtr.FinalizerPtr a
なお,mallocとnewForeignPtr,finalizerFreeの三つをセットで使用する手間を省くため,「do { p <- malloc; newForeignPtr finalizerFree p }」と等価なmallocFreignPtrという関数も用意されています。
Prelude Foreign.ForeignPtr> :t mallocForeignPtr mallocForeignPtr :: (Foreign.Storable.Storable a) => IO (ForeignPtr a)
実際にForeignPtrを使ってみましょう。
{-# LANGUAGE ForeignFunctionInterface #-} module ForeignPtrExample where import Foreign.ForeignPtr import Foreign.Marshal.Alloc import Foreign.Marshal.Utils import Foreign.Ptr import Foreign.Storable testForeignPtr :: Int -> IO (ForeignPtr Int) testForeignPtr val = do p <- new val print $ show p ++ " is allocated." func <- gcFunc $ \ptr -> do free ptr print $ show ptr ++ " is deallocated." newForeignPtr func p foreign import ccall "wrapper" gcFunc :: (Ptr a -> IO ()) -> IO (FunPtr (Ptr a -> IO ()))
GCによる後処理の実行が明確になるよう,GC時の処理を行う際にメッセージを出力するようにするとよいでしょう。ここでは,finalizerFreeでメモリーを解放する代わりに,実行メッセージの出力をアクションとして追加した関数ポインタfuncを使用することにします。
*ForeignPtrExample> testForeignPtr 365 "0x021625e0 is allocated." 0x021625e0 *ForeignPtrExample> testForeignPtr 366 "0x021626b8 is allocated." 0x021626b8 *ForeignPtrExample> :r Ok, modules loaded: ForeignPtrExample. *ForeignPtrExample> print "force GC." "force GC." *ForeignPtrExample> "0x021626b8 is deallocated." "0x021625e0 is deallocated."
少し間が空いていますが,testForeignPtrを使って確保した0x021625e0と0x021626b8のメモリーがあとで解放されているのがわかります。途中で:reloadコマンド(:r)を使っているのは,testForeignPtrによって作成されたForeignPtrへの参照(変数)を現在の評価環境から外すことで,GCの実行を促すためです。
ここまでで紹介した関数はいずれも,GCに実行させる後処理をForeignPtr作成時に結び付けていました。しかし,特定のポインタに適した後処理は,必ずしもそのポインタの種類によって確定するとは限りません。状況に応じて,別の後処理が必要になることがあります。そのような場合,これまで示してきたような「ForeignPtr作成時に後処理を決定する」という方法では柔軟性に欠けます。
そこで,「後処理と結び付けずに,とりあえずPtrからForeinPtrを作成する関数」のnewForeignPtr_と「既存のForeignPtrに対し,後処理を後から加える関数」のaddForeignPtrFinalizer*が提供されています。
Prelude Foreign.ForeignPtr> :t newForeignPtr_ newForeignPtr_ :: GHC.Ptr.Ptr a -> IO (ForeignPtr a) Prelude Foreign.ForeignPtr> :t addForeignPtrFinalizer addForeignPtrFinalizer :: FinalizerPtr a -> ForeignPtr a -> IO () Prelude Foreign.ForeignPtr> :t addForeignPtrFinalizerEnv addForeignPtrFinalizerEnv :: FinalizerEnvPtr env a -> GHC.Ptr.Ptr env -> ForeignPtr a -> IO ()
addForeignPtrFinalizer*を使うと,上のtestForeignPtrは以下のように書き直せます。
testForeignPtr' :: Int -> IO (ForeignPtr Int) testForeignPtr' val = do p <- new val print $ show p ++ " is allocated." fp <- newForeignPtr finalizerFree p func <- gcFunc $ \ptr -> print $ show ptr ++ " is deallocated." addForeignPtrFinalizer func fp return fp
ただし,addForeignPtrFinalizer*を使って追加される処理は,追加した順番とは逆の順番で実行されることに注意してください。例えば,以下のようにtestForeignPtrの行うべき処理を追加すると,最後に加えた処理が最初に実行されます。
testForeignPtr'' :: Int -> IO (ForeignPtr Int) testForeignPtr'' val = do p <- new val print $ show p ++ " is allocated." fp <- newForeignPtr finalizerFree p func <- gcFunc $ \ptr -> print $ show ptr ++ " is deallocated." addForeignPtrFinalizer func fp func' <- gcFunc $ \ptr -> print $ "This function behaves before deallocating " ++ show ptr ++ "." addForeignPtrFinalizer func' fp return fp
ForeignPtrExample> *testForeignPtr'' 12 "0x02162930 is allocated." 0x02162930 ForeignPtrExample> print "force GC." "force GC." *ForeignPtrExample> "This function behaves before deallocating 0x02162930." "0x02162930 is deallocated."
逆順に追加していくのは,直感的でないように思えます。なぜこのようになっているのでしょうか。それは「ForeignPtrとfinalizerFreeのようなポインタ解放処理をどのように結び付けるべきか」を考えればわかります。
ForeignPtrとポインタ解放処理を結び付ける操作は,なるべく早く行うべきです。この操作を後回しにしていると,うっかり忘れてGC時にメモリーが解放されなくなる確率が高くなるからです。
ここで「addForeignPtrFinalizer*で処理を追加するのと同じ順番」で後処理が行われると仮定してみましょう。ポインタの解放処理を最初に結び付けると,解放済みのポインタを操作しようとする処理を後で付け加えることになります。この順番で実行すると,プログラムは意図していない動作を引き起こすでしょう。ポインタの解放処理は必ず最後に行われなければならないからです。
つまり,最初に結び付けたポインタ解放処理が最後に行われるという逆順での処理が実は妥当なのです。