関数ポインタを利用する
実用的なプログラムを書くには,通常の関数呼び出しだけでは不十分です。コールバック関数を登録する場合などでは,関数ポインタが必要になることがあります。関数ポインタの利用方法を見ていきましょう。
先に示したforeign.hでは,関数ポインタを使ってPoint型に演算を施すapplyPoint関数を提供していました。
typedef void (*PointFunc) (struct Point *pt); void applyPoint (struct Point* pt, PointFunc func);
applyPoint関数を利用するには,Haskellの関数をCの関数ではなく関数ポインタに変換しなければなりません。どうすればよいでしょうか?
Cのコードで泥臭く変換することもできますが,FFIには関数ポインタを使用するための宣言がすでに用意されています。foreign import宣言でwrapperという名前を使うことで,Haskellの関数を関数ポインタとして呼び出すための補助関数を定義できます。
{-# INCLUDE "foreign.h" #-} {-# LANGUAGE ForeignFunctionInterface #-} {-# LANGUAGE EmptyDataDecls #-} module FFIExample where import Foreign.Ptr data Point = Point Int Int data CPoint newPoint (Point x y) = c_newPoint x y main = printPoint (Point 16 34) printPoint pt = do cpt <- newPoint pt func <- c_PointFunc $ printCPoint c_applyPoint cpt func printCPoint pt = do x <- c_getPointX pt y <- c_getPointY pt print $ "Point " ++ show x ++ " " ++ show y foreign import ccall "wrapper" c_PointFunc :: (Ptr CPoint -> IO ()) -> IO (FunPtr (Ptr CPoint -> IO ())) foreign import ccall "applyPoint" c_applyPoint :: Ptr CPoint -> FunPtr (Ptr CPoint -> IO ()) -> IO () foreign import ccall "newPoint" c_newPoint :: Int -> Int -> IO (Ptr CPoint) foreign import ccall "getPointX" c_getPointX :: Ptr CPoint -> IO Int foreign import ccall "getPointY" c_getPointY :: Ptr CPoint -> IO Int
wrapperで定義される関数の型は,必ず「ft -> IO (FunPtr ft)」になることに注意してください。IOにくるまない型を宣言しようとするとエラーになります。同様に,関数ポインタはPtr型ではなくForeign.Ptrモジュールで定義されているFunPtr型で表さなければなりません。
Prelude Foreign.Ptr> :i FunPtr data FunPtr a = GHC.Ptr.FunPtr GHC.Prim.Addr# -- Defined in GHC.Ptr instance Eq (FunPtr a) -- Defined in GHC.Ptr instance Ord (FunPtr a) -- Defined in GHC.Ptr instance Show (FunPtr a) -- Defined in GHC.Ptr
c_applyPointはc_PointFuncを使って作成された関数ポインタを呼び出すので,「CPoint -> FunPtr (Ptr CPoint -> IO ()) -> IO ()」のようにFunPtr型の値を取るよう定義されています。
結果を確認してみましょう。
*FFIExample> pt <- newPoint (Point 23 232) 0x02164888 *FFIExample> printCPoint pt "Point 23 232" *FFIExample> printPoint (Point 23 232) "Point 23 232"
同じ値が表示されることから,printPoint関数の内部でc_applyPointがprintCPointを呼び出しているのがわかると思います。
HugsやGHCiではこれで十分なのですが,GHCを使って実行可能ファイルやライブラリを作成する場合には,気をつけなければならないことがもう一つあります。「foreign import "wrapper"」が宣言されている場合,GHCはモジュール名に_stub接尾辞をつけたスタブ・ファイル(stub file)を自動的に作成して使用します。
$ ls FFIExample.hs FFIExample_stub.h HscExample.hsc foreign.h FFIExample_stub.c FFIExample_stub.o foreign.c foreign.o
このため,実行可能ファイルやライブラリの作成時には,スタブ・ファイルをリンクしなければなりません。
$ ghc -c FFIExample.hs foreign.c -main-is FFIExample $ ghc FFIExample.hs foreign.c FFIExample_stub.o -main-is FFIExample compilation IS NOT required $ ./main "Point 16 34"
そうしなければ,実行可能ファイルの作成時にリンク・エラーが生じます。第10回で紹介した--makeオプションを使うと,スタブ・ファイルを自動的に探索してリンクしてくれるので,スタブ・ファイルを指定する手間を省けます(参考リンク)。
$ ghc FFIExample.hs foreign.c -main-is FFIExample --make Linking FFIExample.exe ... $ ./FFIExample "Point 16 34"
FFIには,関数ポインタをHaskellの関数に変換するための宣言も用意されています。dynamicを使うことで,関数ポインタをHaskellの関数として呼び出すための(FunPtr ft) -> ft型の補助関数を定義できます。これを使うことで,上のコードをc_applyPointを使わないように書き換えられます。
{-# INCLUDE "foreign.h" #-} {-# LANGUAGE ForeignFunctionInterface #-} {-# LANGUAGE EmptyDataDecls #-} module FFIExample where import Foreign.Ptr data Point = Point Int Int data CPoint newPoint (Point x y) = c_newPoint x y main = printPoint (Point 16 34) printPoint pt = do cpt <- newPoint pt func <- c_PointFunc $ printCPoint -- c_applyPoint cpt func callbackFunc func cpt printCPoint pt = do x <- c_getPointX pt y <- c_getPointY pt print $ "Point " ++ show x ++ " " ++ show y foreign import ccall "dynamic" callbackFunc :: FunPtr (Ptr CPoint -> IO ()) -> Ptr CPoint -> IO () foreign import ccall "wrapper" c_PointFunc :: (Ptr CPoint -> IO ()) -> IO (FunPtr (Ptr CPoint -> IO ())) foreign import ccall "applyPoint" c_applyPoint :: Ptr CPoint -> FunPtr (Ptr CPoint -> IO ()) -> IO () foreign import ccall "newPoint" c_newPoint :: Int -> Int -> IO (Ptr CPoint) foreign import ccall "getPointX" c_getPointX :: Ptr CPoint -> IO Int foreign import ccall "getPointY" c_getPointY :: Ptr CPoint -> IO Int
$ ghc FFIExample.hs foreign.c -main-is FFIExample --make Linking FFIExample.exe ... $ ./FFIExample "Point 16 34"
FFIの様々な呼び出し規約(calling convention) 今回は標準的なC関数の呼び出ししか行わなかったので,ccallだけを使いました。FFIの仕様では,オプションとして他にもいくつかの呼び出し規約が用意されています。Windows APIで使われるstdcall,JavaやJava仮想マシン(JVM)を対象としたjvm,.NETフレームワークを対象としたdotnet,C++を対象にしたcplusplusの四つです(参考リンク)。 こうした野心的な仕様には心躍るものがありますが,実際にこうした呼び出しをすべて網羅するのは大変です。例えば,GHCのような進化の早い処理系では,JVMや .NETの呼び出しに必要なバイトコードを生成する部分を作成しても,それが古くならないようメンテナンスするのは難しいでしょう。実際,GHCを.NETに対応させるプロジェクトは何度も現れては消えていきました。2008年5月には,すでに過去の遺物となっていたExtended IL(ILX)やJavaを作成するコードが取り除かれてしまいました(参考リンク1,参考リンク2,参考リンク3)。また,C++の呼び出しを可能にするには,コンパイラやバージョンごとに異なる名前修飾の規則(mangling rules)をどうにかして解決する方法が必要になります(参考リンク)。このため,GHCは現状ではccallとstdcallにしか対応していません。 Hugsではこれに加えてdotnetが使えるはずなのですが,Haskellコミュニティで実際に .NETプログラミングにこの機能を活用しているという話は聞きません(参考リンク)。 というわけで,2008年7月現在,仕様のいくつかを満たす処理系はあっても,完全に満たす処理系はないようです。 もちろん,仕様を完全に満たすのではなく,別の方向に向かうのも処理系のあり方の一つでしょう。様々な環境用のバイトコードを出力するYhcではprimitiveという名前が使われています。Haskellに独自仕様を追加した処理系や,別の言語実行環境上に構築されたHaskell処理系であれば,新しい呼び出し規約を採用するのもありだと思います(参考リンク)。 なお,こうした呼び出し規約は単体で提供されるわけではなく,プログラミングをサポートするために周辺のライブラリも付属するのが普通です。stdcallに対応する形で,Win32パッケージのGraphics.Win32モジュールやSystem.Win32モジュールで基本的な型や関数などがあらかじめ提供されています。Hugsでは,dotnet共に,.NETのオブジェクトをうまく扱うためのDotnetライブラリが提供されています(参考リンク1,参考リンク2)。またccallに対しては,今回扱ったもののほかにも,FFIを使ったプログラミングを楽にするための関数が豊富に用意されています。例えば,Cの配列をHaskellのリストとして扱うためのForeign.Marshal.Arrayモジュール,Cの文字列(char型の配列)を扱うためのForeign.C.Stringモジュールなどがあります。 |
著者紹介 shelarcy どうでしたか? 皆さんが考えていたよりもHaskellでのFFIの使用はずっと簡単だったのではないでしょうか? 今回見てきたように,HaskellのFFIは「どうしても必要なとき以外は,Cのコードを記述しなくてもHaskellのコードだけで完結できる」ようデザインされています。この特徴は,外部の関数を利用するプログラムの作成を幾分か楽にしてくれます。しかし,外部の関数を呼び出すことの本質的な難しさを解決するものではないことは,くれぐれも忘れないでください。 Haskellの可能性を広げ,より実用的な言語にするためにFFIは必要不可欠な存在です。一方で,FFIの存在は,Haskellだけで完結している場合には起こり得ないsegmentation faultなどのエラーを持ち込み,Haskellプログラムを不安定にします。この問題に対処するには,外部関数が記述されている言語やライブラリ,FFIで用意されている解決策などに関する様々なバッドノウハウが必要になります。こうした問題と解決策については,次回以降で具体的に説明していきたいと思います。 |