您可以利用涉及 FFI 的功能执行以下两件事:
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]。这意味着不再可能将两个不同的值编组为同一类型,例如你不能再同时拥有两者
instance FFI Int CInt where
instance FFI Int32 CInt where
这样做的原因是因为freeFFI
不能按照您定义的方式使用;无法仅从 ffitype 中确定选择哪个实例。或者,您可以将类型更改为freeFFI :: ffitype -> basic -> IO ()
, 或更好?)freeFFI :: ffitype -> IO basic
。那么你就根本不需要fundeps了。
分配 FunPtr 的唯一方法是使用“foreign import”语句,该语句仅适用于完全实例化的类型。您还需要启用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]让我们定义两个助手:
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
和以前一样,不可能使这些函数具有多态性,这导致了我对这个系统最大的保留。这是很大的开销,因为您需要为每种类型的函数创建单独的包装器和实例。除非您要进行大量的功能编组,否则我会严重怀疑这是否值得付出努力。
这就是编组函数的方式,但是如果您想让它们可用于调用代码怎么办?这个另一个过程是出口该功能,我们已经开发了大部分必需的功能。
导出的函数必须具有可编组类型,就像FunPtr
s。我们可以简单地重复使用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
不幸的是,这种设置仅适用于单参数函数。为了支持所有功能,让我们再创建一个类:
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,因此您可以删除它以使一种基本类型能够映射到多个 ffitype。这样做的原因之一是将所有大小的整数映射到整数,尽管toFFI
实施将会有损。
[2] 稍微简化。你可以编组一个函数String -> String
FFI 类型CString -> IO CString
。但现在你无法转换CString -> IO CString
函数返回到String -> String
因为IO的返回类型。