我用Haskell编写了一个库,我希望它可以在C程序中使用。我已经阅读了一些关于使用foreign export ccall
命令和Foreign
模块的文档。
我见过一些示例,例如this one,但这些示例使用常见的C类型,例如Int
或Double
。
在我的库中,我创建了一些数据类型,如:
data OrdSymb = SEQ
| SLT
| SGT
或使用提供的类型递归:
data MyType a =
TypeDouble Double
| TypeInt Int
| TypeVar a
| TypeAdd (MyType a) (MyType a)
但我没有找到如何使用/导出这些类型与FFI。
如何将自己的数据类型导出到C并在foreign
声明中使用它们来导出我的函数?
答案 0 :(得分:3)
您问题的简短回答是:
hsc2hs
或c2hs
或两者都可能有所帮助。这些工具不是为将 Haskell函数导出到C而设计的,但它们仍然有用。现在,这是一个(非常,非常)长的答案,你的问题实际上会给你一些合作的东西。首先是一些需要进口:
-- file: MyLib1.hs
module MyLib1 where
import Control.Exception.Base
import Foreign
import Foreign.C
让我们从您的第一个数据类型开始,这是一个带有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构造函数表示(OrdSymb
,SLT
和SEQ
)和C之间为SGT
类型显式映射表示为整数(-1,0或1)。您可以使用几个普通函数(例如toOrdSymb
和fromOrdSymb
)执行此操作,但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
的相应方法,并使用上面定义的CInt
和toEnum
方法执行实际转型。
请注意,此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.hs
,mylib.h
和example1.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绑定只会更容易一些。祝你好运!