我强烈建议使用预处理器。我喜欢 c2hs,但 hsc2hs 很常见,因为它包含在 ghc 中。绿卡似乎被放弃了。
要回答您的问题:
1) 是的,通过 Storable 实例的定义。使用 Storable 是通过 FFI 传递数据的唯一安全机制。Storable 实例定义了如何在 Haskell 类型和原始内存(Haskell Ptr、ForeignPtr 或 StablePtr 或 C 指针)之间编组数据。这是一个例子:
data PlateC = PlateC {
numX :: Int,
numY :: Int,
v1 :: Double,
v2 :: Double } deriving (Eq, Show)
instance Storable PlateC where
alignment _ = alignment (undefined :: CDouble)
sizeOf _ = {#sizeof PlateC#}
peek p =
PlateC <$> fmap fI ({#get PlateC.numX #} p)
<*> fmap fI ({#get PlateC.numY #} p)
<*> fmap realToFrac ({#get PlateC.v1 #} p)
<*> fmap realToFrac ({#get PlateC.v2 #} p)
poke p (PlateC xv yv v1v v2v) = do
{#set PlateC.numX #} p (fI xv)
{#set PlateC.numY #} p (fI yv)
{#set PlateC.v1 #} p (realToFrac v1v)
{#set PlateC.v2 #} p (realToFrac v2v)
{# ... #}
片段是 c2hs 代码 。fI
是fromIntegral
。get 和 set 片段中的值从包含的标头引用以下结构,而不是同名的 Haskell 类型:
struct PlateCTag ;
typedef struct PlateCTag {
int numX;
int numY;
double v1;
double v2;
} PlateC ;
c2hs 将其转换为以下普通的 Haskell:
instance Storable PlateC where
alignment _ = alignment (undefined :: CDouble)
sizeOf _ = 24
peek p =
PlateC <$> fmap fI ((\ptr -> do {peekByteOff ptr 0 ::IO CInt}) p)
<*> fmap fI ((\ptr -> do {peekByteOff ptr 4 ::IO CInt}) p)
<*> fmap realToFrac ((\ptr -> do {peekByteOff ptr 8 ::IO CDouble}) p)
<*> fmap realToFrac ((\ptr -> do {peekByteOff ptr 16 ::IO CDouble}) p)
poke p (PlateC xv yv v1v v2v) = do
(\ptr val -> do {pokeByteOff ptr 0 (val::CInt)}) p (fI xv)
(\ptr val -> do {pokeByteOff ptr 4 (val::CInt)}) p (fI yv)
(\ptr val -> do {pokeByteOff ptr 8 (val::CDouble)}) p (realToFrac v1v)
(\ptr val -> do {pokeByteOff ptr 16 (val::CDouble)}) p (realToFrac v2v)
偏移量当然取决于架构,因此使用预处理器可以让您编写可移植的代码。
您可以通过为您的数据类型( 、 等)分配空间new
并将malloc
数据poke
放入 Ptr(或 ForeignPtr)中来使用它。
2)这是真正的内存布局。
peek
3) 用/读/写会受到惩罚poke
。如果您有大量数据,最好只转换您需要的数据,例如,只从 C 数组中读取一个元素,而不是将整个数组编组为 Haskell 列表。
4) 语法取决于您选择的预处理器。 c2hs 文档。 hsc2hs 文档。令人困惑的是,hsc2hs 使用语法#stuff
or #{stuff}
,而 c2hs 使用{#stuff #}
.
5)@sclv 的建议也是我会做的。写一个 Storable 实例并保存一个指向数据的指针。您可以编写 C 函数来完成所有工作并通过 FFI 调用它们,或者(不太好)使用 peek 和 poke 编写低级 Haskell 来仅对您需要的数据部分进行操作。来回编组整个事情(即调用peek
或poke
在整个数据结构上)将是昂贵的,但如果你只传递指针,成本将是最小的。
通过 FFI 调用导入的函数会受到很大的惩罚,除非它们被标记为“不安全”。声明导入“不安全”意味着该函数不应回调到 Haskell 或未定义的行为结果。如果您使用并发性或并行性,这也意味着具有相同功能(即 CPU)的所有 Haskell 线程将阻塞,直到调用返回,因此它应该很快返回。如果这些条件是可接受的,则“不安全”调用相对较快。
Hackage 上有很多处理这类事情的包。我可以推荐hsndfile和hCsound作为 c2hs 的良好实践。如果您查看与您熟悉的小型 C 库的绑定,这可能会更容易。