从Haskell释放C运行时分配的内存

smi*_*dha 5 c garbage-collection haskell

我正在学习如何使用Haskell的C FFI。

假设我正在调用C函数,该函数创建一个对象,然后返回指向该对象的指针。我是否可以使用Haskell运行时释放此内存free?(我指的是Haskell的free 不是 C的free

考虑以下代码:

{-# LANGUAGE ForeignFunctionInterface #-}
module Main where
import Prelude hiding (exp)
import Foreign.Marshal.Alloc
import Foreign.Storable
import Foreign.C.Types
import Foreign.Ptr 
import Foreign.Marshal.Array

foreign import ccall "get_non_freed_array"  c_get_non_freed_array :: CInt -> IO (Ptr CInt) -- An array initialized

main :: IO()
main = do
  let numelements = 5
  ptr <-  c_get_non_freed_array  numelements
  w0  <-  peek $ advancePtr ptr 0 
  w1  <-  peek $ advancePtr ptr 1 
  w2  <-  peek $ advancePtr ptr 2 
  w3  <-  peek $ advancePtr ptr 3 
  w4  <-  peek $ advancePtr ptr 4 
  print [w0, w1, w2, w3, w4]
  return ()
Run Code Online (Sandbox Code Playgroud)

get_non_freed_array我在C99中编写的功能如下

#include "test.h"
#include <stdlib.h>
// return a memory block that is not freed.
int* get_non_freed_array(int n)
{
  int* ptr = (int*) malloc(sizeof(int)*n);

  for(int i=0 ; i<n ; ++i){
          ptr[i] = i*i;
   }
  return ptr;
}
Run Code Online (Sandbox Code Playgroud)

test.h仅包含一行,其中包含get_non_freed_arrayHaskell的FFI对其进行访问的功能签名。)

我很困惑,因为我不知道当从Haskell的运行时调用该C函数“完成”运行时,C运行时分配的内存是否被垃圾回收了。我的意思是,如果是另一个C函数调用它,那么我知道该内存可以安全使用,但是由于Haskell函数正在调用get_non_freed_array,所以我不知道这是否成立。

即使上面的Haskell代码可以打印正确的结果,我也不知道C函数返回的内存是否可以安全地通过使用ptr

如果安全的话,我们可以从Haskell本身释放此内存吗?还是我必须destroy_array(int* ptr)在test.c内编写另一个C函数,然后从Haskell调用它?


编辑:简而言之,我需要更多有关在Haskell中编写代码时如何使用指向在C函数内部创建的对象的指针的信息。

Zet*_*eta 2

TL;DR:使用正确的相应函数(例如 C'smalloc和 C's free)释放内存,并且更喜欢alloca-style 函数,或者ForeignPtr如果这是不可能的。


APtr只是一个地址。通常Addr#指向垃圾收集机械之外。有了这些知识,我们就可以回答您的第一个隐含问题:不,当 C 函数完成时,C 运行时分配的内存不会被垃圾收集。

其次,从 Haskell 本身释放内存通常并不安全。您已经使用过 C's malloc,所以您应该使用 C's free。虽然 Haskell 的当前实现free使用 C 语言,但您不能指望它,就像 Foreign.Marshal.Alloc.freeHaskell 变体那样。

注意我说的是一般情况。GHC 中的当前实现仅使用 C 对应函数,但不应依赖于此,而应使用相应的函数。这符合你的destroy_array方法:幸运的是,这并不难:

foreign import ccall "stdlib.h free" c_free :: Ptr CInt -> IO ()
Run Code Online (Sandbox Code Playgroud)

您的 C 文档应该包含一个注释,该注释free是正确的函数。现在,你可以main这样写:

main :: IO()
main = do
  let numelements = 5
  ptr <-  c_get_non_freed_array  numelements
  w0  <-  peek $ advancePtr ptr 0 
  w1  <-  peek $ advancePtr ptr 1 
  w2  <-  peek $ advancePtr ptr 2 
  w3  <-  peek $ advancePtr ptr 3 
  w4  <-  peek $ advancePtr ptr 4 
  print [w0, w1, w2, w3, w4]
  c_free ptr
  return ()
Run Code Online (Sandbox Code Playgroud)

但这与 C 中一样容易出错。您已经要求进行垃圾收集。这就是 a 的ForeignPtr用途。Ptr 我们可以使用以下命令从法线创建一个newForeignPtr

newForeignPtr :: FinalizerPtr a -> Ptr a -> IO (ForeignPtr a)
Run Code Online (Sandbox Code Playgroud)

Source FinalizerPtr(type FinalizerPtr a = FunPtr (Ptr a -> IO ())是一个函数指针。所以我们需要稍微调整一下之前的导入:

--                                    v
foreign import ccall unsafe "stdlib.h &free" c_free_ptr :: FinalizerPtr CInt
--                                    ^
Run Code Online (Sandbox Code Playgroud)

现在我们可以创建您的数组:

makeArray :: Int -> ForeignPtr CInt
makeArray n = c_get_non_freed_array >>= newForeignPtr c_free_ptr
Run Code Online (Sandbox Code Playgroud)

为了实际使用ForeignPtr,我们需要使用withForeignPtr

main :: IO()
main = do
  let numelements = 5
  fptr <-  makeArray  numelements
  withForeignPtr fptr $ \ptr -> do
      w0  <-  peek $ advancePtr ptr 0 
      w1  <-  peek $ advancePtr ptr 1 
      w2  <-  peek $ advancePtr ptr 2 
      w3  <-  peek $ advancePtr ptr 3 
      w4  <-  peek $ advancePtr ptr 4 
      print [w0, w1, w2, w3, w4]
  return ()
Run Code Online (Sandbox Code Playgroud)

Ptr和的区别ForeignPtr在于后者会调用终结器。但这个例子有点做作。alloca*如果你只想分配一些东西,使用它的函数,然后返回,这些函数会让你的生活变得更容易,例如

withArrayLen xs $ \n ptr -> do
   c_fast_sort n ptr
   peekArray n ptr
Run Code Online (Sandbox Code Playgroud)

这些Foreign.Marshal.*模块有许多有用的功能。

最后一点:使用原始内存可能会带来麻烦并且成为错误源。如果您创建一个供公众使用的库,请将其隐藏。