Haskell外来类型的习惯用法?

Geo*_*rge 12 c haskell ffi

问题:从Haskell类型转到外部类型并返回需要大量的样板代码.

例如,假设我们正在使用以下Haskell数据结构:

data HS_DataStructure = HS_DataStructure {
       a1 :: String    
    ,  b1 :: String
    ,  c1 :: Int
}
Run Code Online (Sandbox Code Playgroud)

为了使这个数据结构进入C的土地,我们需要考虑它的struct类比:

typedef struct {
        char *a;
        char *b;
        int   c;
} c_struct;
Run Code Online (Sandbox Code Playgroud)

但是为了将这样的结构从Haskell传递给C,我们必须转换HS_DataStructure为以下内容:

data HS_Struct = HS_Struct { 
      a :: CString
    , b :: CString
    , c :: CInt
} deriving Show
Run Code Online (Sandbox Code Playgroud)

然后我们必须做HS_Struct一个例子Storable:

instance Storable HS_Struct where
    sizeOf    _ = #{size c_struct}
    alignment _ = alignment (undefined :: CString)

poke p c_struct = do
    #{poke c_struct, a} p $ a c_struct
    #{poke c_struct, b} p $ b c_struct
    #{poke c_struct, c} p $ c c_struct

peek p = return HS_Struct
          `ap` (#{peek c_struct, a} p)
          `ap` (#{peek c_struct, b} p)
          `ap` (#{peek c_struct, c} p)
Run Code Online (Sandbox Code Playgroud)

(在上面我使用的是hs2c语法).

最后,为了在HS_Struct和之间进行转换HS_DataStructure,我们不得不使用以下辅助函数(!):

makeStruct :: HS_DataStructure -> IO (HS_Struct)
makeStruct hsds = do str1 <- newCString (a1 hsds)
                             str2 <- newCString (b1 hsds)
                             jreturn (HS_Struct str1 str2 (c1 hsds))

makeDataStructure :: Ptr (HS_Struct) -> IO (HS_DataStructure)
makeDataStructure p = do hss <- peek p
                          hs1 <- peekCString (a hss)j
                          hs2 <- peekCString (b hss)
                         return (HS_DataStructure hs1 hs2 (c hss))
Run Code Online (Sandbox Code Playgroud)

这似乎是在Haskell和C之间来回传播的疯狂数量的样板.

问题

  1. 有没有办法最小化上面的样板?
  2. 随着涉及FFI的重量Haskell的项目,它是地道的只是让步,主要使用Haskell的C型(即CInt,CString,等等)?这至少可以为您节省必须在类型之间来回转换的麻烦.

Mok*_*sha 1

“惯用地”,我认为没有办法绕过 C 和 Haskell 之间编组值的样板。不过,您可能会发现一些对您的问题有用的答案。在为 Haskell 用户编写并贡献了许多包装 C 库的 之后,我强烈建议使用Bindings-dsl库来解决您的问题,该库底层使用 hsc2hs 。具体来说,回答你的问题:

1. 有什么办法可以最小化上面的样板吗?

您可以消除其中一些,但在大多数情况下,C 类型和 Haskell 类型之间的封送需要小心确保 Haskell 值保持良好的基础。这是一个功能,而不是一个错误,因为 C 类型本质上是不同的表示形式。例如,对于StringCString,您需要指定如果调用会发生什么

makeStruct $ HS_DataStructure (repeat 'a') (repeat 'b') 0
Run Code Online (Sandbox Code Playgroud)

...或如何处理内存分配(您的示例将在没有相应deleteStruct函数的情况下泄漏)。同样,也存在对 和 的整数语义的Int担忧CIntCInt如果你得到的 a超出了范围,会发生什么Int?夹钳?裹?这些答案通常是特定于应用程序的,并且与其他库的互操作性要求这些不变量适用于所有 Haskell 程序。

使用绑定-dsl ,我们至少可以通过使用以下内容定义文件来摆脱编写自己的Storable实例的需要:.hsc

module MyModule where

#include <c_struct.h>

#starttype struct HS_Struct
#field a, CString
#field b, CString
#field c, CInt
#stoptype
Run Code Online (Sandbox Code Playgroud)

如果您将此模块添加到 cabal 文件中,cabal 应该认识到它需要使用hsc2hs,并将正确编译它,添加所有附加实例。查看上面的任何链接作为示例。您的代码makeDataStructure也可以变得(稍微)简单一些:

makeDataStructure :: Ptr HS_Struct -> IO HS_DataStructure
makeDataStructure p = do
    HS_Struct ca cb cc <- peek p
    HS_DataStructure <$> peekCString ca <*> peekCString cb <*> fromIntegral cc
Run Code Online (Sandbox Code Playgroud)

使用真正的“样板文件减少”优势bindings-DSL来自于头文件中存在的Haskell 端 FFI 函数定义。

2. 对于涉及大量 FFI 的 Haskell 项目,是否习惯于放弃并主要使用 Haskell 的 C 类型(即 CInt、CString 等)?这至少可以省去您在类型之间来回转换的麻烦。

让某些东西成为“惯用”有点主观(因此容易受到自行车停放和把关的影响),因此您对这个问题的任何答案都应该持保留态度,并且可能不是最适合这个网站的。在我看来,我不认为主要使用 C 类型是惯用的。这些类型的存在只是为了与 C 接口,并且 Haskell 运行时是围绕 Haskell 类型进行优化的。如果您发现自己编写了如此多的 FFI,以至于您觉得需要主要使用 FFI 类型,那么这可能是一个很好的迹象,表明您应该编写一个 C 库。

当然,例外情况是,如果您正在编写一个包装 C 库的 Haskell 库。在这种情况下,我认为 Haskell 库的全部要点是以 Haskell 运行时和 C 库接口之间一致的方式实现样板。