当在C中管理(释放)分配时,Haskell运行时的垃圾收集器问题

10
我希望使用Haskell的FFI功能,在C和Haskell之间共享数据(在最简单的情况下,是一个整数数组)。C端创建数据(相应地分配内存),但在释放之前从不修改它,因此我认为以下方法会比较“安全”:
  • 数据创建后,C函数传递数组的长度和指向其开头的指针。
  • 在Haskell端,我们创建ForeignPtr,设置一个终结器,该终结器调用一个C函数以释放指针。
  • 我们使用该外部指针构建一个Vector,可以在Haskell代码中(不可变地)使用。
但是,使用这种方法会导致相当非确定性的崩溃。小例子往往能够正常工作,但是“一旦GC启动”,我就开始收到各种错误,从段错误到GHC的GC“疏散”部分中thisthis行的“barf”。
我在这里做错了什么?做类似这样的事情的“正确方法”是什么?

一个例子

我有一个带有以下声明的C头文件:
typedef struct CVector {
    const int32_t *pointer;
    size_t length;
} Vector;

void create_c_vector(struct CVector *vector);
void free_buffer(void *buff);

使用c2hs,从以下.chs文件生成Haskell代码:

import Foreign.C.Types
import Foreign.Concurrent
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable

import qualified Data.Vector.Storable as V

#include <cvector.h>


data ForeignVector = ForeignVector
  { pointerFV  :: Ptr CInt
  , lengthFV   :: CULong
  }

instance Storable ForeignVector where
  sizeOf _ = {#sizeof CVector #}
  alignment _ = {#alignof CVector #}
  peek p =
    ForeignVector
      <$> {#get CVector->pointer #} p
      <*> {#get CVector->length #} p
  poke p (ForeignVector vecP l) =
    do {#set CVector.pointer #} p (castPtr vecP)
       {#set CVector.length #} p l

peekUnit :: Storable a => Ptr () -> IO a
peekUnit = peek . castPtr

{#fun create_c_vector as ^ { alloca- `ForeignVector' peekUnit*} -> `()' #}
{#fun free_buffer as ^ { `Ptr ()' } -> `()' #}

fromForeign :: ForeignVector -> IO (V.Vector CInt)
fromForeign (ForeignVector p l) =
  V.unsafeFromForeignPtr0
    <$> newForeignPtr p (freeBuffer . castPtr $ p)
    <*> pure (fromIntegral l)

createVector :: IO (V.Vector CInt)
createVector = fromForeign =<< createCVector

我进行的一个特定测试在调用几千次createVector后产生了internal error: evacuate: strange closure type 177
PS:以下是我为什么想使用Foreign.Concurrent.newForeignPtr而不是更“标准”的Foreign.ForeignPtr.newForeignPtr的原因:在我预期的一些更复杂的情况下,在释放指针时,还应清理可能依赖于从Haskell传递的参数的其他内容。因此,我希望有一个“具有多个参数的终结器”,并将一个部分应用程序作为实际终结器传递。这意味着我不能使用C函数的指针作为终结器。虽然我已经阅读到可以使用“包装”机制从Haskell函数中生成所需的FinalizerPtr用于终结器,但根据documentation,以这种方式获取的函数指针需要使用freeHaskellFunPtr显式地释放,并且我不想做这方面的记录。
PPS:以下是一个包含上述示例的完整源代码(包括可重现上述错误的可执行代码)的base64编码tarball。
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
1RzRbhaYtmiLtmh/afsPAHfp2gAuAAA=

1
我曾经遇到过类似的崩溃,当时闭包被某人(也就是我)写入了超出数组范围的内容。我还特别注明了所有可能破坏像castPtr这样类型的东西的类型,这可能有助于您思考问题;是否有可能pokecastPtr没有按照您的预期执行,或者ForeignPtr被丢弃并在您预期之前释放了缓冲区?(在一个特别令人沮丧的情况下,我不得不使用touchForeignPtr来保持ForeignPtr的存活状态。)此外,您使用的GHC版本是哪个? - Jon Purdy
谢谢!看起来多态的alloca确实是问题所在:由c2hs生成的createVector相当于类似于alloca $ \ ptr -> createCVector'_ ptr >> peek ptr的东西,其中createCVector'_ :: Ptr () -> IO (), 这意味着alloca只分配了足够容纳一个单元的空间。 将内部编组器更改为alloca' f = alloca $ f . (castPtr :: Ptr ForeignVector -> Ptr ())似乎解决了这个问题。您是否想发布带有您的评论和解决方案的答案,以便我可以接受它?(顺便说一下,我已经使用GHC 8.10.4和GHC 8.4.4进行了测试。) - aclow
太棒了!完成了。希望这能帮助未来的某个人节省时间和挫折感。 (你想打赌是我吗?) - Jon Purdy
1个回答

3

本文摘自 我的早前评论

你可能存在类型转换错误或poke问题。我推荐一个防御性指南和调试技巧,即:

显式注释可能破坏类型的所有内容的类型。这样,您始终知道正在操作的是什么类型。即使pokecastPtrunsafeCoerce当前具有我预期的类型,但在代码重构过程中可能会发生变化。即使这不能解决问题,它至少可以帮助您对问题进行思考。

例如,我曾经在将空终止符写入字节缓冲区时...,因为我使用的是'\NUL'而不是char,而是Char——32位!原因是pokeByteOff是多态的:它具有类型(Storable a) => Ptr b -> Int -> a -> IO (),而不是… => Ptr a -> …

这也是您代码中的情况! @aclow所说

c2hs生成的createVectoralloca $ \ ptr -> createCVector'_ ptr >> peek ptr等效,其中createCVector'_ :: Ptr () -> IO (),这意味着alloca仅分配足以容纳一个单元的空间。将内部调用程序更改为alloca' f = alloca $ f . (castPtr :: Ptr ForeignVector -> Ptr ())似乎可以解决问题。

可能存在但未出现的问题:

当闭包被某人(即我)写超出数组范围时,我遇到了类似的崩溃。如果进行任何不带边界检查的写操作,则将它们替换为带有检查的版本可能有助于查看是否会出现异常而不是堆损坏。在某种程度上,这就是在此处发生的情况,但是写入的是alloca分配的区域,而不是数组。

另外,考虑生命周期问题:即ForeignPtr是否可能在你预期之前被删除并释放缓冲区,导致使用后释放的情况。在一个特别令人沮丧的情况下,我不得不使用touchForeignPtr来保持ForeignPtr的存活。


1
供日后参考:看起来实际上有一种方法可以说服 c2hscreateCVector'_ 导入为一个以 Ptr ForeignVector 作为参数的函数。需要使用类似 {#pointer *CVector as FVPtr -> ForeignVector #} 的钩子将 C 结构体 CVectorForeignVector 关联起来(这使得 c2hs 定义了 type FVPtr = C2HSImp.Ptr (ForeignVector) 并在导入的函数中使用它)。完成这个步骤后,{#fun create_c_vector as ^ {alloca- \ForeignVector' peek*} -> `()' #}(即多态的 allocapeek`)似乎可以按预期工作。 - aclow

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