Haskell FFI:在C程序中使用数据类型

时间:2017-10-15 21:08:47

标签: c haskell ffi

我用Haskell编写了一个库,我希望它可以在C程序中使用。我已经阅读了一些关于使用foreign export ccall命令和Foreign模块的文档。

我见过一些示例,例如this one,但这些示例使用常见的C类型,例如IntDouble

在我的库中,我创建了一些数据类型,如:

data OrdSymb = SEQ
             | SLT
             | SGT

或使用提供的类型递归:

data MyType a =
        TypeDouble Double
      | TypeInt Int
      | TypeVar a 
      | TypeAdd (MyType a) (MyType a) 

但我没有找到如何使用/导出这些类型与FFI。

如何将自己的数据类型导出到C并在foreign声明中使用它们来导出我的函数?

1 个答案:

答案 0 :(得分:3)

您问题的简短回答是:

  • 正如此处以及对相关问题的评论中所建议的那样,首先要完全设计您的C API,包括您希望能够从C代码中使用的C数据表示和C函数。这不是一个微不足道的步骤。您在此处做出的设计决策将影响您将这些Haskell类型输出到C并返回时所采用的方法(AKA编组)。
  • 使用" C-to-Haskell"帮助自动化尺寸,对齐和结构构件访问的螺母和螺栓。 hsc2hsc2hs或两者都可能有所帮助。这些工具不是为 Haskell函数导出到C而设计的,但它们仍然有用。
  • 期望花费大量时间阅读整个FFI子系统,研究上述工具生成的输出,并编写大量C胶。任何非平凡的语言绑定都很复杂,而且您正在尝试公开以高级别编写的库。语言通过低级别的绑定"语言使它变得更具挑战性。

现在,这是一个(非常,非常)长的答案,你的问题实际上会给你一些合作的东西。首先是一些需要进口:

-- file: MyLib1.hs
module MyLib1 where

import Control.Exception.Base
import Foreign
import Foreign.C

Enum的绑定

让我们从您的第一个数据类型开始,这是一个带有0-ary构造函数的简单求和类型:

data OrdSymb = SEQ | SLT | SGT deriving (Show, Eq)

具体来说,我们假设我们也有一些符号:

newtype Symbol = Symbol String

我们希望使用以下签名公开Haskell函数:

compareSymb :: Symbol -> Symbol -> OrdSymb
compareSymb (Symbol x) (Symbol y) =
  case compare x y of { EQ -> SEQ; LT -> SLT; GT -> SGT }

checkSymb :: Symbol -> OrdSymb -> Symbol -> Bool
checkSymb x ord y = compareSymb x y == ord

不可否认,checkSymb是愚蠢的,但我希望展示能够产生OrdSymb结果和接受OrdSymb参数的函数示例。

这里是我们希望拥有这些数据类型和功能的C接口。具有0-ary构造函数的sum类型的自然C表示是枚举,因此我们得到如下内容:

enum ord_symb {
        SLT = -1,
        SEQ = 0,
        SGT = 1
};

符号可以用指向NUL终止的C字符串的指针来表示:

typedef char* symbol;

,导出函数的签名如下所示:

enum ord_symb compare_symb(symbol x, symbol y);
bool check_symb(symbol x, enum ord_symb ord, symbol y);

这里是如何完全手动创建C语言绑定,没有C-to-Haskell助手。这有点单调乏味,但看到它会帮助你理解幕后发生的事情。

我们需要在Haskell构造函数表示(OrdSymbSLTSEQ)和C之间为SGT类型显式映射表示为整数(-1,0或1)。您可以使用几个普通函数(例如toOrdSymbfromOrdSymb)执行此操作,但Haskell为Enum类提供了一些符合此描述的函数:

instance Enum OrdSymb where

  toEnum (-1) = SLT
  toEnum 0    = SEQ
  toEnum 1    = SGT

  fromEnum SLT = -1
  fromEnum SEQ = 0
  fromEnum SGT = 1

出于文档目的,定义表示C端类型enum ord_symb的类型也很有帮助。 C标准表示枚举与int具有相同的表示形式,因此我们将写入:

type C_OrdSymb = CInt

现在,因为OrdSymb是一个简单类型,所以可能有意义创建一个Storable实例,可以将其值封送到C {{1}在预分配的内存中。这看起来像这样:

enum ord_symb

我们使用了帮助函数:

instance Storable OrdSymb where
  sizeOf _ = sizeOf (undefined :: C_OrdSymb)
  alignment _ = alignment (undefined :: C_OrdSymb)
  peek ptr = genToEnum <$> peek (castPtr ptr :: Ptr C_OrdSymb)
  poke ptr val = poke (castPtr ptr :: Ptr C_OrdSymb) (genFromEnum val)

此处genToEnum :: (Integral a, Enum b) => a -> b genToEnum = toEnum . fromIntegral genFromEnum :: (Integral a, Enum b) => b -> a genFromEnum = fromIntegral . fromEnum peek只包含普通poke的相应方法,并使用上面定义的CInttoEnum方法执行实际转型。

请注意,此fromEnum实例在技术上并不需要。我们可以在没有这样的实例的情况下封送Storable进出C OrdSymb,并且实际上在下面的示例中,我们将做什么。但是,如果我们以后必须使用包含enum ord_symb成员的C结构,或者如果我们发现我们正在编组Storable s的数组,则enum ord_symb可以派上用场什么的。

然而,值得注意的是 - 通常来说 - 与C不同的对象需要enum ord_symb,制作Storable的东西并没有神奇地处理编组的所有细节。特别是,如果我们尝试为Storable编写Storable个实例,我们就会遇到麻烦。 Symbol s应该具有预定的长度,因此Storable不应该检查它的参数。但是,sizeOf的大小取决于底层字符串,因此除非我们决定实现最大字符串长度并以此方式存储所有Symbol,否则我们不应该使用这里有Symbol个实例。相反,让我们在没有Storable类的好处的情况下为Symbol编写一些编组函数:

Storable

请注意,我们不会捅#34;符号,因为我们通常没有预先分配的正确大小的缓冲区,我们在其中写入符号。相反,当我们想要将peekSymbol :: Ptr Symbol -> IO Symbol peekSymbol ptr = Symbol <$> peekCString (castPtr ptr) newSymbol :: Symbol -> IO (Ptr Symbol) newSymbol (Symbol str) = castPtr <$> newCString str freeSymbol :: Ptr Symbol -> IO () freeSymbol = free 编组为C时,我们需要为其分配一个新的C字符串,这就是Symbol所做的事情。为避免内存泄漏,我们需要在完成符号后调用newSymbol(或仅freeSymbol)符号(或让我们的C绑定用户知道他们负责在指针上调用C函数free。这也意味着编写一个帮助程序可能会有所帮助,该帮助程序可用于包装使用编组符号的计算而不会泄漏内存。同样,这是我们在这个例子中实际使用的东西,但是定义它是一件有用的事情:

free

现在,我们可以通过编写执行编组的包装器来导出我们的Haskell函数:

withSymbol :: Symbol -> (Ptr Symbol -> IO a) -> IO a
withSymbol sym = bracket (newSymbol sym) freeSymbol

请注意,最后一行中的mylib_compare_symb :: Ptr Symbol -> Ptr Symbol -> IO C_OrdSymb mylib_compare_symb px py = do x <- peekSymbol px y <- peekSymbol py return $ genFromEnum (compareSymb x y) mylib_check_symb :: Ptr Symbol -> C_OrdSymb -> Ptr Symbol -> IO CInt mylib_check_symb px ord py = do x <- peekSymbol px y <- peekSymbol py return $ genFromEnum (checkSymb x (genToEnum ord) y) 用于Haskell genFromEnum类型的Enum实例,将false / true变为0/1。

此外,值得注意的是,对于这些包装器,我们根本没有使用任何Bool个实例!

最后,我们可以将包装函数导出到C。

Storable

如果您将所有上述Haskell代码放入foreign export ccall mylib_compare_symb :: Ptr Symbol -> Ptr Symbol -> IO C_OrdSymb foreign export ccall mylib_check_symb :: Ptr Symbol -> C_OrdSymb -> Ptr Symbol -> IO CInt ,请创建 MyLib1.hsmylib.hexample1.c内容如下:

ffitypes.cabal

// file: mylib.h
#ifndef MYLIB_H
#define MYLIB_H

enum ord_symb {
        SLT = -1,
        SEQ = 0,
        SGT = 1
};
typedef char* symbol;   // NUL-terminated string

// don't need these signatures -- they'll be autogenerated into
// MyLib1_stub.h
//enum ord_symb compare_symb(symbol x, symbol y);
//bool check_symb(symbol x, enum ord_symb ord, symbol y);

#endif

// file: example1.c
#include <HsFFI.h>
#include "MyLib1_stub.h"
#include <stdio.h>

#include "mylib.h"

int main(int argc, char *argv[])
{
    hs_init(&argc, &argv);

    symbol foo = "foo";
    symbol bar = "bar";

    printf("%s\n", mylib_compare_symb(foo, bar) == SGT ? "pass" : "fail");
    printf("%s\n", mylib_check_symb(foo, SGT, bar) ? "pass" : "fail");
    printf("%s\n", mylib_check_symb(foo, SEQ, bar) ? "fail" : "pass");

    hs_exit();
    return 0;
}

并将所有内容放在新的-- file: ffitypes.cabal name: ffitypes version: 0.1.0.0 cabal-version: >= 1.22 build-type: Simple executable example1 main-is: example1.c other-modules: MyLib1 include-dirs: . includes: mylib.h build-depends: base default-language: Haskell2010 cc-options: -Wall -O ghc-options: -Wall -Wno-incomplete-patterns -O 目录中。然后,从该目录:

ffitypes

应该可以运行这个例子。

对参数化的递归类型进行编组

现在,让我们转向更复杂的$ stack init $ stack build $ stack exec example1 。我已将MyType更改为Int,因此它会在典型平台上与Int32匹配。

CInt

这是一个带有一元和二元构造函数的sum类型,一个任意类型参数data MyType a = TypeDouble Double | TypeInt Int32 | TypeVar a | TypeAdd (MyType a) (MyType a) 和递归结构,所以非常复杂。同样,从指定具体的C实现开始,这一点非常重要。 C联合可用于存储复杂的和类型,但我们也希望标记&#34;标记&#34;带有枚举的联合来指示联合所代表的构造函数,因此C类型看起来像这样:

a

请注意,要允许C绑定与包含多个可能参数typedef struct mytype_s { enum mytype_cons_e { TYPEDOUBLE, TYPEINT, TYPEVAR, TYPEADD } mytype_cons; union { double type_double; int type_int; void* type_var; struct { struct mytype_s *left; struct mytype_s *right; } type_add; } mytype_value; } mytype; 的{​​{1}}一起使用,我们需要为MyType a联盟成员使用a

完全手动编写void*的编组功能非常痛苦且容易出错。有很多关于C结构的确切尺寸,对齐方式和布局的详细信息,您需要正确完成。相反,我们将使用type_var帮助程序包。我们将从新MyType

的顶部开始一个小序言
c2hs

MyLib2.chs包非常适合使用枚举。例如,使用此包为-- file: MyLib2.chs module MyLib2 where import Foreign import Foreign.C #include "mylib.h" 标记创建编组基础结构如下所示:

c2hs

请注意,此自动从C标头enum mytype_cons_e中检索定义,创建一个等同于的Haskell定义:

-- file: MyLib2.chs
{#enum mytype_cons_e as MyTypeCons {}#}

并定义所需的mylib.h实例,以将Haskell构造函数映射到C端的整数值。在这里使用我们的广义-- data MyTypeCons = TYPEDOUBLE | TYPEINT | etc. Enum助手也很有用:

toEnum

现在,让我们看一下编组数据类型:

fromEnum

来自genToEnum :: (Integral a, Enum b) => a -> b genToEnum = toEnum . fromIntegral genFromEnum :: (Integral a, Enum b) => b -> a genFromEnum = fromIntegral . fromEnum 。一个警告:这些实现假设递归构造函数data MyType a = TypeDouble Double | TypeInt Int32 | TypeVar a | TypeAdd (MyType a) (MyType a) 及其C模拟struct mytype_s从未用于创建&#34;周期&#34;在C或Haskell方面。处理TypeAdd递归意义上的递归数据结构需要采用不同的方法。

因为type_add是一个固定长度的结构,你可能会认为它是let x = 0:x实例的一个很好的候选者,但结果并非如此。由于struct mytype_s联合成员中的嵌入指针和Storable成员中的递归指针,因此无法为type_var编写合理的type_add实例。我们可以写一个:

Storable

指针已经明确指出。当我们对此进行编组时,我们假设我们已经编组了“孩子”这样的孩子&#34;节点,并指向我们可以编组到结构的指针。对于MyType构造函数,我本可以写这个:

data C_MyType a =
  C_TypeDouble Double
  | C_TypeInt Int32
  | C_TypeVar (Ptr a)
  | C_TypeAdd (Ptr (MyType a)) (Ptr (MyType a))

这并不重要,因为我们可以在C_TypeAdd -- C_TypeAdd (Ptr (C_MyType a)) (Ptr (C_MyType a)) 之间来回自由地Ptr来回传播MyType。我决定使用我的定义,因为它摆脱了两个C_MyType调用。

castPtr的{​​{1}}实例如下所示。请注意Storable如何让我们自动查找大小,路线和偏移。我们必须手动计算这些全部。否则。

C_MyType

c2hs的{​​{1}}实例排除在外,真实 instance Storable (C_MyType a) where sizeOf _ = {#sizeof mytype_s#} alignment _ = {#alignof mytype_s#} peek p = do typ <- genToEnum <$> {#get struct mytype_s->mytype_cons#} p case typ of TYPEDOUBLE -> C_TypeDouble . (\(CDouble x) -> x) <$> {#get struct mytype_s->mytype_value.type_double#} p TYPEINT -> C_TypeInt . (\(CInt x) -> x) <$> {#get struct mytype_s->mytype_value.type_int #} p TYPEVAR -> C_TypeVar . castPtr <$> {#get struct mytype_s->mytype_value.type_var#} p TYPEADD -> do q1 <- {#get struct mytype_s->mytype_value.type_add.left#} p q2 <- {#get struct mytype_s->mytype_value.type_add.right#} p return $ C_TypeAdd (castPtr q1) (castPtr q2) poke p t = case t of C_TypeDouble x -> do tag TYPEDOUBLE {#set struct mytype_s->mytype_value.type_double#} p (CDouble x) C_TypeInt x -> do tag TYPEINT {#set struct mytype_s->mytype_value.type_int #} p (CInt x) C_TypeVar q -> do tag TYPEVAR {#set struct mytype_s->mytype_value.type_var #} p (castPtr q) C_TypeAdd q1 q2 -> do tag TYPEADD {#set struct mytype_s->mytype_value.type_add.left #} p (castPtr q1) {#set struct mytype_s->mytype_value.type_add.right#} p (castPtr q2) where tag = {#set struct mytype_s->mytype_cons#} p . genFromEnum 的编组功能看起来非常干净:

Storable

请注意我们需要为C_MyType类型使用帮助程序。每当我们想为MyType制作peekMyType :: (Ptr a -> IO a) -> Ptr (MyType a) -> IO (MyType a) peekMyType peekA p = do ct <- peek (castPtr p) case ct of C_TypeDouble x -> return $ TypeDouble x C_TypeInt x -> return $ TypeInt x C_TypeVar q -> TypeVar <$> peekA q C_TypeAdd q1 q2 -> do t1 <- peekMyType peekA q1 t2 <- peekMyType peekA q2 return $ TypeAdd t1 t2 newMyType :: (a -> IO (Ptr a)) -> MyType a -> IO (Ptr (MyType a)) newMyType newA t = do p <- malloc case t of TypeDouble x -> poke p (C_TypeDouble x) TypeInt x -> poke p (C_TypeInt x) TypeVar v -> poke p . C_TypeVar =<< newA v TypeAdd t1 t2 -> do q1 <- newMyType newA t1 q2 <- newMyType newA t2 poke p (C_TypeAdd q1 q2) return (castPtr p) -- case from Ptr C_MyType to Ptr MyType freeMyType :: (Ptr a -> IO ()) -> Ptr (MyType a) -> IO () freeMyType freeA p = do ct <- peek (castPtr p) case ct of C_TypeVar q -> freeA q C_TypeAdd q1 q2 -> do freeMyType freeA q1 freeMyType freeA q2 _ -> return () -- no children to free free p 时,我们就需要为a类型提供量身定制的newMyType。有可能将它变成一个类型类,甚至可以为所有MyType a创建一个实例,但我还没有在这里完成。

现在,假设我们有一个Haskell函数,它使用我们想要导出到C的所有这些数据类型:

newA

使用先前定义的辅助函数:

a

我们在Storable a中还需要其他一些东西。首先,我们将使用replaceSymbols :: OrdSymb -> Symbol -> Symbol -> MyType Symbol -> MyType Symbol replaceSymbols ord sym1 sym2 = go where go (TypeVar s) | checkSymb s ord sym1 = TypeVar sym2 go (TypeAdd t1 t2) = TypeAdd (go t1) (go t2) go rest = rest 来定义compareSymb :: Symbol -> Symbol -> OrdSymb compareSymb (Symbol x) (Symbol y) = case compare x y of { EQ -> SEQ; LT -> SLT; GT -> SGT } checkSymb :: Symbol -> OrdSymb -> Symbol -> Bool checkSymb x ord y = compareSymb x y == ord 类型(同样,这会自动生成关联的MyLib2.chs):

c2hs

和从OrdSymb复制的符号编组代码:

data OrdSymb

然后,我们可以编写以下C包装器:

{#enum ord_symb as OrdSymb {} deriving (Show, Eq)#}
type C_OrdSymb = CInt

鉴于这会返回一个malloced数据结构,它还有助于提供一个导出的函数来释放它:

MyLib1.hs

让我们出口:

newtype Symbol = Symbol String

peekSymbol :: Ptr Symbol -> IO Symbol
peekSymbol ptr = Symbol <$> peekCString (castPtr ptr)

newSymbol :: Symbol -> IO (Ptr Symbol)
newSymbol (Symbol str) = castPtr <$> newCString str

freeSymbol :: Ptr Symbol -> IO ()
freeSymbol = free

如果您在本节中采用所有Haskell代码,从mylib_replace_symbols :: C_OrdSymb -> Ptr Symbol -> Ptr Symbol -> Ptr (MyType Symbol) -> IO (Ptr (MyType Symbol)) mylib_replace_symbols ord psym1 psym2 pt = do sym1 <- peekSymbol psym1 sym2 <- peekSymbol psym2 t <- peekMyType peekSymbol pt let t' = replaceSymbols (genToEnum ord) sym1 sym2 t newMyType newSymbol t' 行开始并将其放入mylib_free_mytype_symbol :: Ptr (MyType Symbol) -> IO () mylib_free_mytype_symbol = freeMyType freeSymbol ,则创建/修改以下文件:

foreign export ccall mylib_replace_symbols
  :: C_OrdSymb -> Ptr Symbol -> Ptr Symbol
       -> Ptr (MyType Symbol) -> IO (Ptr (MyType Symbol))
foreign export ccall mylib_free_mytype_symbol
  :: Ptr (MyType Symbol) -> IO ()

module MyLib2

并将MyLib2.chs子句添加到您的Cabal文件中:

// file: mylib.h
#ifndef MYLIB_H
#define MYLIB_H

enum ord_symb {
    SLT = -1,
    SEQ = 0,
    SGT = 1
};
typedef char* symbol;   // NUL-terminated string

typedef struct mytype_s {
    enum mytype_cons_e {
        TYPEDOUBLE,
        TYPEINT,
        TYPEVAR,
        TYPEADD
    } mytype_cons;
    union {
        double type_double;
        int type_int;
        void* type_var;
        struct {
            struct mytype_s *left;
            struct mytype_s *right;
        } type_add;
    } mytype_value;
} mytype;

#endif

并将它们全部放在// file: example2.c #include <HsFFI.h> #include "MyLib2_stub.h" #include <stdio.h> #include "mylib.h" // AST for: 1.0 + foo mytype node1 = { TYPEDOUBLE, {type_double: 1.0} }; mytype node2 = { TYPEVAR, {type_var: "foo"} }; mytype root = { TYPEADD, {type_add: {&node1, &node2} } }; int main(int argc, char *argv[]) { hs_init(&argc, &argv); mytype *p1 = mylib_replace_symbols(SEQ, "foo", "bar", &root); printf("%s\n", // should print "bar" (char*) p1->mytype_value.type_add.right->mytype_value.type_var); mytype *p2 = mylib_replace_symbols(SEQ, "quux", "bar", &root); printf("%s\n", // unchanged -- should still be "foo" (char*) p2->mytype_value.type_add.right->mytype_value.type_var); mylib_free_mytype_symbol(p1); mylib_free_mytype_symbol(p2); hs_exit(); return 0; } 目录中,然后您应该可以executable example2-- file: ffitypes.cabal name: ffitypes version: 0.1.0.0 cabal-version: >= 1.22 build-type: Simple executable example1 main-is: example1.c other-modules: MyLib1 include-dirs: . includes: mylib.h build-depends: base default-language: Haskell2010 cc-options: -Wall -O ghc-options: -Wall -Wno-incomplete-patterns -O executable example2 main-is: example2.c other-modules: MyLib2 include-dirs: . includes: mylib.h build-depends: base build-tools: c2hs default-language: Haskell2010 cc-options: -Wall -O ghc-options: -Wall -Wno-incomplete-patterns -O

语言绑定很难!

从上面的代码中可以看出,为Haskell库创建简单的C绑定需要做很多工作。如果它有任何安慰,那么为C库创建Haskell绑定只会更容易一些。祝你好运!