在Haskell和C之间交换结构化数据

23

首先,我是一个 Haskell 初学者。

我计划将 Haskell 集成到实时游戏的 C 代码中。Haskell 负责逻辑,C 负责渲染。为了实现这一目标,我需要在每个 tick(至少每秒 30 次)之间相互传递巨大且结构复杂的数据(游戏状态)。因此,传递数据应该轻量级。这些状态数据可能会在内存上的连续空间上布局。Haskell 和 C 的部分都应该自由访问状态的每个区域。

在最好的情况下,传递数据的成本可以是复制指向内存的指针。在最坏的情况下,需要进行整体数据复制和转换。

我正在阅读有关 Haskell FFI(http://www.haskell.org/haskellwiki/FFICookBook#Working_with_structs)的文档。其中,Haskell 代码看起来明确地指定了内存布局。

我有几个问题:

  1. Haskell 是否能够明确指定内存布局(与 C 结构体完全匹配)?
  2. 这是真正的内存布局吗?是否需要任何类型的转换(会影响性能)?
  3. 如果问题2是正确的,明确指定内存布局是否会有任何性能损失?
  4. 什么是语法 #{alignment foo}?在哪里可以找到相关文档?
  5. 如果我想以最佳性能传递大量数据,应该如何做到?

*PS:我所说的明确内存布局特性只是 C# 的 [StructLayout] 属性。它可以明确指定内存中的位置和大小。 http://www.developerfusion.com/article/84519/mastering-structs-in-c/

我不确定 Haskell 是否具有与 C 结构体字段匹配的相应语言结构。


1
请参见https://dev59.com/xG035IYBdhLWcg3wBLXb。 - Don Stewart
3个回答

25

我强烈建议使用预处理器。我喜欢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 代码。fIfromIntegral。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)

偏移量当然取决于体系结构,因此使用预处理器允许您编写可移植的代码。

您可以通过为数据类型分配空间(newmalloc等)并将数据输入到 Ptr(或 ForeignPtr)中来使用它。

2)这是真正的内存布局。

3)使用peek/poke读写会有代价。如果您有大量数据,最好只转换所需的部分,例如从C数组中仅读取一个元素,而不是将整个数组编组为Haskell列表。

4)语法取决于您选择的预处理器。c2hs文档hsc2hs文档。令人困惑的是,hsc2hs使用语法#stuff#{stuff},而c2hs则使用{#stuff #}

5)@sclv的建议也是我会做的。编写一个Storable实例并保留指向数据的指针。您可以编写C函数来完成所有工作,并通过FFI调用它们,或者(不太好)编写使用peek和poke仅操作所需数据部分的低级Haskell。来回编组整个数据结构(即在整个数据结构上调用peekpoke)将是昂贵的,但是如果只传递指针,则成本将很小。

通过FFI调用导入函数会有很大的惩罚,除非它们标记为“unsafe”。声明导入为“unsafe”意味着该函数不应调用回Haskell或未定义行为结果。如果您正在使用并发或并行性,它还意味着在同一能力(即CPU)上的所有Haskell线程都将阻塞,直到调用返回,因此应该相对快速地返回。如果这些条件可接受,“unsafe”调用相对较快。

Hackage上有很多处理此类事情的包。我建议使用hsndfilehCsound作为c2hs的良好实践。不过,如果您熟悉的C库的绑定更容易。


谢谢!我意识到Haskell数据结构与C不兼容。它不能在没有(任何类型的)转换的情况下传递给C。这意味着使用共享结构内存模型是不可能的。因此,我决定将其视为完全分离的服务器/客户端模型,使用FFI作为通信通道传递高度压缩的状态数据。而且,我会继续寻找另一种更兼容的语言。还有...调用带有unsafe的导入函数是否确实没有开销? - eonil
1
@Eonil,这听起来像是一种可靠、高效的架构。不幸的是,我不知道不安全的外部导入的确切开销,但我经常在实时应用程序中使用它们而没有任何问题。 - John L

7
尽管您可以为严格的未打包的Haskell结构获得确定性内存布局,但没有保证并且这是一个非常非常糟糕的想法。如果您愿意接受转换,那么可以使用Storeable:http://www.haskell.org/ghc/docs/6.12.3/html/libraries/base-4.2.0.2/Foreign-Storable.html。我会构建C结构,然后使用FFI构建直接在其上运行的Haskell函数,而不是尝试生成它们的Haskell“等效项”。或者,您可以决定只需要将一小部分信息传递给C--不是整个游戏状态,而只是关于世界中哪些对象在哪里的少量信息,并且您实际上如何绘制它们的信息仅存在于C方程式的数据中。然后,在Haskell中执行所有逻辑,操作本地Haskell结构,仅将C实际上需要渲染的这个微小子集投影到C世界中。编辑:我应该补充说明,矩阵和其他常见的C结构已经拥有优秀的库/绑定,这些库/绑定在C端保持了重负。

谢谢。我也在考虑你提到的方法。这种数据压缩仍然需要传递相同类型的数据... FFI调用是否没有性能惩罚?如果调用成本不高,直接调用是一个有吸引力的选项。 - eonil

2
hsc2hs”,“c→hs”和“Green Card”都提供了自动的Haskell⇆C结构体查看/填充或者编组功能。我建议使用它们,而不是手动确定大小和偏移量并在Haskell中使用指针操作,尽管后者也是可能的。
1. 据我所知,没有这样的功能,如果我理解你的意思正确的话。Haskell没有内置处理外部聚合数据结构的功能。 2. 3. 4. 正如该页面所描述的那样,这是通过一些C魔法实现的“hsc2hs”。

谢谢。我的意思是手动指定每个数据字段。(就像C#的[StructLayout]属性)http://www.developerfusion.com/article/84519/mastering-structs-in-c/ - eonil
非常明确地说,没有像StructLayout属性这样的东西。 - sclv
@sclv 谢谢。我会考虑你提到的内容。 - eonil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接