Haskell 中 FFI 调用的类型自动转换

发布于 2024-09-11 07:57:45 字数 494 浏览 9 评论 0原文

我定义了以下模块来帮助我进行 FFI 函数导出:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, TypeSynonymInstances #-}
module ExportFFI where

import Foreign
import Foreign.C


class FFI basic ffitype | basic -> ffitype where
    toFFI :: basic -> IO ffitype
    fromFFI :: ffitype -> IO basic
    freeFFI :: ffitype -> IO ()

instance FFI String CString where
    toFFI = newCString
    fromFFI = peekCString
    freeFFI = free

我正在努力处理函数实例。有人可以帮助我吗?

I have defined the following module to help me with FFI function export:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, TypeSynonymInstances #-}
module ExportFFI where

import Foreign
import Foreign.C


class FFI basic ffitype | basic -> ffitype where
    toFFI :: basic -> IO ffitype
    fromFFI :: ffitype -> IO basic
    freeFFI :: ffitype -> IO ()

instance FFI String CString where
    toFFI = newCString
    fromFFI = peekCString
    freeFFI = free

I'm struggling with the instance for functions. Can someone help me?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

三生池水覆流年 2024-09-18 07:57:45

您可以利用涉及 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 ->基本-> IO(),或者(更好?)freeFFI::ffitype -> IO 基本。那么你就根本不需要fundeps了。

分配 FunPtr 的唯一方法是使用“foreign import”语句,该语句仅适用于完全实例化的类型。您还需要启用 ForeignFunctionInterface 扩展。因此,应返回 IO (FunPtr x) 的 toFFI 函数不能在函数类型上实现多态。换句话说,您需要这个:

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);包装StrFn = 包装Fn。请注意,unwrapFn 使用“Control.Exception.bracket”来确保在发生异常时释放资源。忽略这一点,您可以编写 unwrapFn fn = toFFI >=> fn >=>来自FFI;看看与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 一样。我们可以简单地重新使用 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 -> StringCString -> 的 FFI 类型IO CString。但现在您无法转换 CString -> IO CString函数返回String -> String因为IO中的返回类型。

There are two things you can do with functions involving the FFI:
1) Marshalling: this means converting a function to a type that can be exported through the FFI. This accomplished by FunPtr.
2) Exporting: this means creating a means for non-Haskell code to call into a Haskell function.

Your FFI class helps with marshalling, and first I create a few sample instances of how to marshal functions.

This is untested, but it compiles and I expect it would work. First, let's change the class slightly:

class FFI basic ffitype | basic -> ffitype, ffitype -> basic where
    toFFI :: basic -> IO ffitype
    fromFFI :: ffitype -> IO basic
    freeFFI :: ffitype -> IO ()

This says that given the type of either "basic" or "ffitype", the other is fixed[1]. This means it's no longer possible to marshal two different values to the same type, e.g. you can no longer have both

instance FFI Int CInt where

instance FFI Int32 CInt where

The reason for this is because freeFFI can't be used as you've defined it; there's no way to determine which instance to select from just the ffitype. Alternatively you could change the type to freeFFI :: ffitype -> basic -> IO (), or (better?) freeFFI :: ffitype -> IO basic. Then you wouldn't need fundeps at all.

The only way to allocate a FunPtr is with a "foreign import" statement, which only works with fully instantiated types. You also need to enable the ForeignFunctionInterface extension. As a result the toFFI function, which should return an IO (FunPtr x), can't be polymorphic over function types. In other words, you'd need this:

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

for every different function type you want to marshal. You also need the FlexibleInstances extension for this instance. There are a few restrictions imposed by the FFI: every type must be a marshallable foreign type, and the function return type must be either a marshallable foreign type or an IO action which returns a marshallable foreign type.

For non-marshallable types (e.g. Strings) you need something slightly more complex. First of all, since marshalling happens in IO you can only marshal functions that result in an IO action.
If you want to marshal pure functions, e.g. (String -> String), you need to lift them to the form (String -> IO String).[2] Let's define two helpers:

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)

These convert the types of functions to the appropriate marshalled values, e.g. wrapStrFn :: (String -> IO String) -> (CString -> IO CString); wrapStrFn = wrapFn. Note that unwrapFn uses "Control.Exception.bracket" to ensure the resource is freed in case of exceptions. Ignoring this you could write unwrapFn fn = toFFI >=> fn >=> fromFFI; see the similarity to wrapFn.

Now that we have these helpers we can start to write instances:

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

As before, it's not possible to make these functions polymorphic, which leads to my biggest reservation about this system. It's a lot of overhead because you need to create separate wrappers and instances for each type of function. Unless you're doing a lot of marshalling of functions, I would seriously doubt it's worth the effort.

That's how you can marshal functions, but what if you want to make them available to calling code? This other process is exporting the function, and we've already developed most of what's necessary.

Exported functions must have marshallable types, just like FunPtrs. We can simply re-use the wrapFn to do this. To export a few functions all you need to do is wrap them with wrapFn and export the wrapped versions:

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

Unfortunately this setup only works for single-argument functions. To support all functions, let's make another class:

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

Now we can use exportFunction for functions with 1 and 2 arguments:

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

Now you just need to write more instances of ExportFunction to automatically convert any function to the appropriate type for exporting. I think this is the best you can do without either either using some type of pre-processor or unsafePerformIO.

[1] Technically, I don't think there's any need for the "basic -> ffitype" fundep, so you could remove it to enable one basic type to map to multiple ffitypes. One reason to do so would be to map all sized ints to Integers, although the toFFI implementations would be lossy.

[2] A slight simplification. You could marshal a function String -> String to the FFI type of CString -> IO CString. But now you can't convert the CString -> IO CString function back to String -> String because of the IO in the return type.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文