问题:从Haskell类型转换为Foreign类型并返回需要大量样板代码。

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

data HS_DataStructure = HS_DataStructure {
       a1 :: String
    ,  b1 :: String
    ,  c1 :: Int
}

为了使此数据结构进入C语言领域,我们将需要考虑其struct类似物:
typedef struct {
        char *a;
        char *b;
        int   c;
} c_struct;

但是为了将这种结构从Haskell传递到C,我们必须将HS_DataStructure转换为以下代码:
data HS_Struct = HS_Struct {
      a :: CString
    , b :: CString
    , c :: CInt
} deriving Show

然后,我们必须使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)

(在上面,我正在使用hs2c语法)。

现在,最后,为了在HS_StructHS_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))

这似乎是在Haskell和C之间来回移动的疯狂模板。

问题
  • 有什么方法可以最小化上面的样板?
  • 对于涉及大量FFI的Haskell项目,仅屈服并​​主要使用Haskell的C类型(即CIntCString等)是惯用的吗?这至少可以节省您在类型之间来回转换的麻烦。
  • 最佳答案

    “习惯上”,我认为没有办法解决C和Haskell之间的值编组问题。但是,对于您的问题,您可能会发现有用的答案。在为为Haskell用户包装了C库的a number of libraries编写并做出贡献之后,我强烈建议您使用bindings-dsl库来解决您的问题,该库在后台使用了hsc2hs。具体来说,为回答您的问题:
    1.有什么方法可以最小化上面的样板?
    您可以消除其中的一些,但是在大多数情况下,在C类型和Haskell类型之间进行编组时需要格外小心,以确保Haskell值保持良好的基础。这是一个功能,而不是错误,因为C类型本质上是不同的表示形式。例如,对于StringCString,您需要指定调用时发生的情况

    makeStruct $ HS_DataStructure (repeat 'a') (repeat 'b') 0
    
    ...或如何处理内存分配(如果没有相应的deleteStruct函数,您的示例将泄漏)。同样,对于IntCInt的整数语义也存在担忧。如果您获得的CInt超出了Int的范围,会发生什么?钳?包?这些答案通常是特定于应用程序的,并且与其他库的互操作性要求不变性适用于所有Haskell程序。
    使用bindings-dsl,我们至少可以通过使用以下内容定义Storable文件来摆脱编写自己的.hsc实例的需要:
    module MyModule where
    
    #include <c_struct.h>
    
    #starttype struct HS_Struct
    #field a, CString
    #field b, CString
    #field c, CInt
    #stoptype
    
    如果将此模块添加到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
    
    使用bindings-DSL的真正“样板减少”优势来自 header 中存在的函数Haskell-side FFI definitions
    2.对于涉及大量FFI的Haskell项目,仅屈服并​​主要使用Haskell的C类型(即CInt,CString等)是惯用的吗?这至少可以节省您在类型之间来回转换的麻烦。
    具有“惯用性”的东西有点主观(因此容易受到自行车脱落和关门的影响),因此,如果您对这个问题有任何答案,都应该加一点盐,可能不是最适合该站点的方法。在我看来,我不认为主要使用C类型是惯用的。这些类型仅用于与C的接口(interface),并且Haskell运行时针对Haskell类型进行了优化。如果您发现自己编写了太多的FFI,以至于需要主要使用FFI类型,则可能很好地表明您应该编写C库。
    当然,如果您正在编写包装了C库的Haskell库,则例外。在这种情况下,我认为Haskell库的重点在于以一种在Haskell运行时和C库接口(interface)之间保持一致的方式来实现样板。

    07-28 03:07
    查看更多