我强烈建议使用预处理器。我喜欢c2hs,但hsc2hs很常见,因为它随ghc一起提供。据了解,Greencard已经被放弃。
回答你的问题:
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
等)并将数据输入到 Ptr(或 ForeignPtr)中来使用它。
2)这是真正的内存布局。
3)使用peek
/poke
读写会有代价。如果您有大量数据,最好只转换所需的部分,例如从C数组中仅读取一个元素,而不是将整个数组编组为Haskell列表。
4)语法取决于您选择的预处理器。c2hs文档。hsc2hs文档。令人困惑的是,hsc2hs使用语法#stuff
或#{stuff}
,而c2hs则使用{#stuff #}
。
5)@sclv的建议也是我会做的。编写一个Storable实例并保留指向数据的指针。您可以编写C函数来完成所有工作,并通过FFI调用它们,或者(不太好)编写使用peek和poke仅操作所需数据部分的低级Haskell。来回编组整个数据结构(即在整个数据结构上调用peek
或poke
)将是昂贵的,但是如果只传递指针,则成本将很小。
通过FFI调用导入函数会有很大的惩罚,除非它们标记为“unsafe”。声明导入为“unsafe”意味着该函数不应调用回Haskell或未定义行为结果。如果您正在使用并发或并行性,它还意味着在同一能力(即CPU)上的所有Haskell线程都将阻塞,直到调用返回,因此应该相对快速地返回。如果这些条件可接受,“unsafe”调用相对较快。
Hackage上有很多处理此类事情的包。我建议使用hsndfile和hCsound作为c2hs的良好实践。不过,如果您熟悉的C库的绑定更容易。