views:

201

answers:

1

Consider the following code snippet

import qualified Foreign.Concurrent
import Foreign.Ptr (nullPtr)

main :: IO ()
main = do
  putStrLn "start"
  a <- Foreign.Concurrent.newForeignPtr nullPtr $
    putStrLn "a was deleted"
  putStrLn "end"

It produces the following output:

start
end

I would had expected to see "a was deleted" somewhere after start..

I don't know what's going on. I have a few guesses:

  • The garbage collector doesn't collect remaining objects when the program finishes
  • putStrLn stops working after main finishes. (btw I tried same thing with foreignly imported puts and got the same results)
  • My understanding of ForeignPtr is lacking
  • GHC bug? (env: GHC 6.10.3, Intel Mac)

When using Foreign.ForeignPtr.newForeignPtr instead of Foreign.Concurrent.newForeignPtr it seems to work:

{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign.C.String (CString, newCString)
import Foreign.ForeignPtr (newForeignPtr)
import Foreign.Ptr (FunPtr)

foreign import ccall "&puts" puts :: FunPtr (CString -> IO ())

main :: IO ()
main = do
  putStrLn "start"
  message <- newCString "a was \"deleted\""
  a <- newForeignPtr puts message
  putStrLn "end"

outputs:

start
end
a was "deleted"
+5  A: 

From the documentation of Foreign.Foreign.newForeignPtr:

Note that there is no guarantee on how soon the finaliser is executed after the last reference was dropped; this depends on the details of the Haskell storage manager. Indeed, there is no guarantee that the finalizer is executed at all; a program may exit with finalizers outstanding.

So you're running into undefined behaviour: i.e., anything can happen, and it may change from platform to platform (as we saw under Windows) or release to release.

The cause of the difference in behaviour you're seeing between the two functions may be hinted at by the documentation for Foreign.Concurrent.newForeignPtr:

These finalizers necessarily run in a separate thread...

If the finalizers for the Foreign.Foreign version of the function use the main thread, but the Foreign.Concurrent ones use a separate thread, it could well be that the main thread shuts down without waiting for other threads to complete their work, so the other threads never get to run the finalization.

Of course, the docs for the Foreign.Concurrent version do claim,

The only guarantee is that the finalizer runs before the program terminates.

I'm not sure that they actually ought to be claiming this, since if the finalizers are running in other threads, they can take an arbitrary amount of time to do their work (even block forever), and thus the main thread would never be able to force the program to exit. That would conflict with this from Control.Concurrent:

In a standalone GHC program, only the main thread is required to terminate in order for the process to terminate. Thus all other forked threads will simply terminate at the same time as the main thread (the terminology for this kind of behaviour is "daemonic threads").

Curt Sampson
you can also block forever in a Foreign.ForeignPtr.newForeignPtr destructor, and you can segfault too etc. so I don't think it's a problem that the program can't exit if your destructors never finish. it's just not something that you should do
yairchu
Well, according to some people it would be a problem. :-) see the quote at the end I just added to the answer. I think it's generally the case in thread systems that being able to force a process to exit is considered more important than giving threads the chance to clean up, at the cost of possibly blocking the exit forever.
Curt Sampson