FFI を含む関数でできることは 2 つあります。 1) マーシャリング: これは、FFI を介してエクスポートできる型に関数を変換することを意味します。これは によって達成されましたFunPtr
。2) エクスポート: これは、非 Haskell コードが Haskell 関数を呼び出す手段を作成することを意味します。
FFI クラスはマーシャリングに役立ちます。まず、関数をマーシャリングする方法のサンプル インスタンスをいくつか作成します。
これはテストされていませんが、コンパイルされ、動作すると予想されます。まず、クラスを少し変更しましょう。
class FFI basic ffitype | basic -> ffitype, ffitype -> basic where
toFFI :: basic -> IO ffitype
fromFFI :: ffitype -> IO basic
freeFFI :: ffitype -> IO ()
これは、「basic」または「ffitype」のいずれかのタイプが与えられた場合、もう一方は固定されていることを示しています[1]。これは、2 つの異なる値を同じ型にマーシャリングすることができなくなったことを意味します。たとえば、両方を持つことはできなくなりました。
instance FFI Int CInt where
instance FFI Int32 CInt where
この理由は、freeFFI
定義したとおりに使用できないためです。ffitype だけから選択するインスタンスを決定する方法はありません。freeFFI :: ffitype -> basic -> IO ()
または、タイプを, or (better?)に変更することもできますfreeFFI :: ffitype -> IO basic
。そうすれば、ファンデプスはまったく必要ありません。
FunPtr を割り当てる唯一の方法は、完全にインスタンス化された型でのみ機能する「外部インポート」ステートメントを使用することです。ForeignFunctionInterface
また、拡張機能を有効にする必要があります。その結果、toFFI
を返すべき関数は、IO (FunPtr x)
関数型に対して多態的であってはなりません。言い換えれば、これが必要になります:
foreign import ccall "wrapper"
mkIntFn :: (Int32 -> Int32) -> IO (FunPtr (Int32 -> Int32))
foreign import ccall "dynamic"
dynIntFn :: FunPtr (Int32 -> Int32) -> (Int32 -> Int32)
instance FFI (Int32 -> Int32) (FunPtr (Int32 -> Int32)) where
toFFI = mkIntFn
fromFFI = return . dynIntFn
freeFFI = freeHaskellFunPtr
マーシャリングするさまざまな関数タイプごとに。FlexibleInstances
このインスタンスの拡張機能も必要です。FFI によって課せられる制限がいくつかあります。すべての型は整列化可能な外部型でなければならず、関数の戻り値の型は整列化可能な外部型または整列化可能な外部型を返す IO アクションのいずれかでなければなりません。
マーシャリング不可能な型 (例えば文字列) の場合は、もう少し複雑なものが必要です。まず第一に、マーシャリングは IO で発生するため、IO アクションが発生する関数のみをマーシャリングできます。(String -> String) などの純粋な関数をマーシャリングする場合は、それらを (String -> IO String) の形式に持ち上げる必要があります[2]。2 つのヘルパーを定義しましょう。
wrapFn :: (FFI a ca, FFI b cb) => (a -> IO b) -> (ca -> IO cb)
wrapFn fn = fromFFI >=> fn >=> toFFI
unwrapFn :: (FFI a ca, FFI b cb) => (ca -> IO cb) -> (a -> IO b)
unwrapFn fn a = bracket (toFFI a) freeFFI (fn >=> fromFFI)
これらは、関数の型を適切なマーシャリングされた値に変換しますwrapStrFn :: (String -> IO String) -> (CString -> IO CString); wrapStrFn = wrapFn
。unwrapFn
「Control.Exception.bracket」を使用して、例外が発生した場合にリソースが確実に解放されることに注意してください。これを無視すると、次のように書くことができますunwrapFn fn = toFFI >=> fn >=> fromFFI
。wrapFn との類似性を参照してください。
これらのヘルパーができたので、インスタンスの作成を開始できます。
foreign import ccall "wrapper"
mkStrFn :: (CString -> IO CString) -> IO (FunPtr (CString -> IO CString))
foreign import ccall "dynamic"
dynStrFn :: FunPtr (CString -> IO CString) -> (CString -> IO CString)
instance FFI (String -> IO String) (FunPtr (CString -> IO CString)) where
toFFI = mkStrFn . wrapFn
fromFFI = return . unwrapFn . dynStrFn
freeFFI = freeHaskellFunPtr
前と同じように、これらの関数をポリモーフィックにすることはできません。これが、このシステムに関する私の最大の懸念につながります。関数の種類ごとに個別のラッパーとインスタンスを作成する必要があるため、オーバーヘッドが大きくなります。多くの関数のマーシャリングを行っていない限り、努力する価値があるとは思えません。
これで関数をマーシャリングできますが、呼び出しコードで関数を使用できるようにしたい場合はどうすればよいでしょうか。この別のプロセスは関数をエクスポートしており、必要なもののほとんどは既に開発されています。
エクスポートされた関数には、s と同様に、マーシャリング可能な型が必要FunPtr
です。これを行うには、単純に を再利用できますwrapFn
。いくつかの関数をエクスポートするには、それらをラップして、ラップwrapFn
されたバージョンをエクスポートするだけです。
f1 :: Int -> Int
f1 = (+2)
f2 :: String -> String
f2 = reverse
f3 :: String -> IO Int
f3 = return . length
foreign export ccall f1Wrapped :: CInt -> IO CInt
f1Wrapped = wrapFn (return . f1)
foreign export ccall f2Wrapped :: CString -> IO CString
f2Wrapped = wrapFn (return . f2)
foreign export ccall f3Wrapped :: CString -> IO CInt
f3Wrapped = wrapFn f3
残念ながら、この設定は引数が 1 つの関数でしか機能しません。すべての機能をサポートするために、別のクラスを作成しましょう。
class ExportFunction a b where
exportFunction :: a -> b
instance (FFI a ca, FFI b cb) => ExportFunction (a->b) (ca -> IO cb) where
exportFunction fn = (wrapFn (return . fn))
instance (FFI a ca, FFI b cb, FFI d cd) => ExportFunction (a->b->d) (ca->cb->IO cd) where
exportFunction fn = \ca cb -> do
a <- fromFFI ca
b <- fromFFI cb
toFFI $ fn a b
exportFunction
これで、引数が 1 と 2 の関数に使用できます。
f4 :: Int -> Int -> Int
f4 = (+)
f4Wrapped :: CInt -> CInt -> IO CInt
f4Wrapped = exportFunction f4
foreign export ccall f4Wrapped :: CInt -> CInt -> IO CInt
f3Wrapped2 = :: CString -> IO CInt
f3Wrapped2 = exportFunction f3
foreign export ccall f3Wrapped2 :: CString -> IO CInt
f3Wrapped2 = exportFunction f3
ExportFunction
これで、任意の関数をエクスポート用の適切な型に自動的に変換するために、のインスタンスをさらに記述するだけで済みます。これは、ある種のプリプロセッサや unsafePerformIO を使用せずに実行できる最善の方法だと思います。
[1] 技術的には、「basic -> ffitype」という Fundep は必要ないと思うので、それを削除して、1 つの基本タイプを複数の ffitype にマップできるようにすることができます。そうする理由の 1 つは、すべてのサイズの int を Integer にマップすることですが、toFFI
実装には損失が伴います。
[2] 少し単純化。String -> String
関数を の FFI タイプにマーシャリングできますCString -> IO CString
。しかし、戻り値の型に IO があるため、CString -> IO CString
関数を元に戻すことはできません。String -> String