haskellmemorymemory-leaksunsafe-perform-io

Is it safe to allocate memory within `unsafeDupablePerformIO`?


Suppose that I have a foreign function:

-- | Turns char* of the given size into a char* of size 16.
doSomethingFfi :: Ptr CUChar -> Ptr CUChar -> CSize -> IO ()
doSomethingFfi = undefined

The function is pure, so I would like to represent it as a pure function in Haskell:

doSomething :: ByteArray bytes => bytes -> bytes
doSomething bs = unsafePerformIO $
  alloc 16 $ \outPtr ->
  withByteArray bs $ \inPtr ->
    doSomethingFfi outPtr inPtr (fromIntegral $ length bs)

(Here I am using alloc from memory.)

My understanding is that the only difference between unsafePerformIO and unsafeDupablePerformIO is that the IO action in the latter can be silently terminated without any cleanup.

In my case above there are, essentially, two IO actions happening: 1. memory allocation; 2. foreign call. I am not concerned about 2, since it is pure, however I am worried about the memory.

Is there any guarantee that the memory allocated this way will not leak if the computation is interrupted silently? If the foreign function also required temporary storage that I had to allocate / clean up and I used alloca for this purpose, would it still be safe to use unsafeDupablePerformIO?


Solution

  • Mostly as I explained in the comments, but not quite:

    alloca

    As alloca is currently implemented, this is safe. alloca is implemented by a call to allocaBytesAligned, which is defined thus:

    allocaBytesAligned :: Int -> Int -> (Ptr a -> IO b) -> IO b
    allocaBytesAligned (I# size) (I# align) action = IO $ \ s0 ->
         case newAlignedPinnedByteArray# size align s0 of { (# s1, mbarr# #) ->
         case unsafeFreezeByteArray# mbarr# s1 of { (# s2, barr#  #) ->
         let addr = Ptr (byteArrayContents# barr#) in
         case action addr     of { IO action' ->
         case action' s2      of { (# s3, r #) ->
         case touch# barr# s3 of { s4 ->
         (# s4, r #)
      }}}}}
    

    This allocates pinned memory in the garbage-collected heap. If your action is aborted early, then the garbage collector will reclaim the memory it allocated sooner or later.

    alloc

    This is not necessarily safe, but may actually be safe in practice. alloc is defined using a class method, allocRet, which different types can implement differently.

    Contrary to my guesses in the comments, the instances defined in memory all seem fine—they too allocate pinned memory. But the class does not document this as a requirement, and in principle someone could allocate memory using Foreign.Marshall.Alloc.malloc, in which case the garbage collector will not take care of the memory automatically. Such a hypothetical implementation would have no way to ensure memory is freed if the computation aborts early.