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

时间:2010-12-21 17:27:29

标签: c syntax haskell ffi

首先,我是Haskell初学者。

我正在计划将Haskell集成到C中用于实时游戏。 Haskell做逻辑,C做渲染。要做到这一点,我必须为每个滴答(每秒至少30次)从彼此传递巨大的复杂结构数据(游戏状态)。所以传递的数据应该是轻量级的。该状态数据可以放在存储器上的顺序空间上。 Haskell和C部分都应该自由地访问各州的每个区域。

在最好的情况下,传递数据的成本可以是复制指向内存的指针。在最坏的情况下,通过转换复制整个数据。

我正在阅读Haskell的FFI(http://www.haskell.org/haskellwiki/FFICookBook#Working_with_structs) Haskell代码看起来明确指定内存布局。

我有几个问题。

  1. Haskell可以明确指定内存布局吗? (与C结构完全匹配)
  2. 这是真正的内存布局吗?或者需要进行任何类型的转换? (绩效惩罚)
  3. 如果Q#2为真,那么在明确指定内存布局时会有任何性能损失吗?
  4. 语法#{alignment foo}是什么?我在哪里可以找到关于此的文件?
  5. 如果我想以最佳性能传递大量数据,我该怎么做?
  6. * PS 我说的显式内存布局功能只是C#的[StructLayout]属性。这明确指定了内存中的位置和大小。 http://www.developerfusion.com/article/84519/mastering-structs-in-c/

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

3 个答案:

答案 0 :(得分:24)

我强烈建议使用预处理器。我喜欢c2hs,但是hsc2hs很常见,因为它包含在ghc中。 Greencard似乎被放弃了。

回答你的问题:

1)是的,通过可存储实例的定义。使用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等)分配空间并将poke数据分配到Ptr(或ForeignPtr)来使用它。

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

3)使用peek / poke进行阅读/写作会受到处罚。如果您有大量数据,最好只转换您需要的数据,例如从C数组中只读取一个元素,而不是将整个数组编组到Haskell列表中。

4)语法取决于您选择的预处理器。 c2hs docshsc2hs docs。令人困惑的是,hsc2​​hs使用语法#stuff#{stuff},而c2hs使用{#stuff #}

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

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

Hackage上有很多软件包处理这类事情。我可以推荐hsndfilehCsound作为c2hs的良好实践。如果你看一下你熟悉的小型C库的绑定可能会更容易。

答案 1 :(得分: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侧的繁重。

答案 2 :(得分:2)

hsc2hsc→hsGreen Card都提供自动Haskell®C结构查看/戳或编组。我建议他们使用手动确定大小和偏移量以及在Haskell中使用指针操作,尽管这也是可能的。

  1. 如果我理解正确的话,据我所知。 Haskell没有任何内置的外部聚合数据结构处理。
  2. 正如该页面描述的那样,hsc2hs带有一些C魔法。