Summary: If you create a C function pointer from a Haskell function with "wrapper", and it throws an exception, bad things happen.
The Haskell FFI is incredibly powerful, allowing you to convert Haskell functions into C function pointers. In this post I'll give a quick example, then go into what happens if the Haskell function throws an exception. First, let's define a C function (and put it in a file called c.c
):
int apply(int(*f)(int), int x)
{
return f(x);
}
The piece int(*f)(int)
says f
is a function of type Int -> Int
. The function apply
is equivalent to $
, restricted to int
- it applies the first argument f
to the second argument x
and returns the result. We can call that in Haskell with:
foreign import ccall apply :: FunPtr (CInt -> IO CInt) -> CInt -> IO CInt
foreign import ccall "wrapper" wrap :: (CInt -> IO CInt) -> IO (FunPtr (CInt -> IO CInt))
main :: IO ()
main = do
f <- wrap $ \x -> return $ x + 20
res <- apply f 22
print res
On the first line we wrap apply
into a Haskell definition, turning a C function pointer into FunPtr
. In the second we define a special "wrapper"
FFI definition - the name "wrapper"
is a specific string which is part of the FFI spec - it converts a Haskell function into a C function pointer. In main
we put these pieces together, and other than the pervasive IO, it looks like the equivalent Haskell.
Note: In real code you should always call freeHaskellFunPtr
after you have finished using a "wrapper"
function, usually using bracket
.
Consequences of Exceptions
What happens if the function we pass to wrap
throws an exception? If you read the GHC manual, you'll find an incomplete link to the FFI spec, which stays silent on the subject. Thinking it through, Haskell has exceptions, but C does not - if the Haskell throws an exception it can't be passed back through C. Haskell can't provide a return value, so it can never resume the C code that called it. The GHC runtime can block indefinitely or kill the thread, both of which are fairly fatal for a program. As a consequence, I strongly recommend never throwing an exception from a function generated by "wrapper"
- but what if we do?
Suggestion: most of the FFI addendum should probably be reproduced in the GHC manual with details around corner cases and exceptions.
Testing Exceptions
First, let's change our wrapped function to wrap $ \x -> fail "finish"
. Running that prints out:
bug.exe: user error (finish)
That seems like a standard exception. However, let's go further and put the entire program inside a finally
, to show we have a normal Haskell exception:
main = flip finally (print "done") $ do
...
The output doesn't change - we never print out "done"
. It seems the exception thrown inside wrap
aborts the program rather than bubbling up.
Suggestion: This error looks like a normal exception, but really isn't. It should say you have violated the wrapper invariant and your program has been violently aborted.
We've encountered bad behaviour, but can we go worse? Yes we can, by adding threads:
main = do
replicateM_ 100 $ do
forkIO $ do
ff <- wrap $ \_ -> fail "die"
print =<< apply ff 12
threadDelay 10000000
Here we spawn 100 threads, each of which does an apply
with an exception, then we wait for 10 seconds. The output is:
bug.exe: user error (die)
bug.exe: user error (die)
bug.exe: warning: too many hs_exit()s
It looks like there is a race condition with the exit path, causing two fatal wrapper exceptions to try and take down the runtime twice.
Suggestion: The hs_exit
bug should be fixed.
Avoiding Exceptions
Now we know we need to avoid throwing exceptions inside "wrapper"
functions, the obvious approach is to wrap them in a catch
, e.g.:
wrap $ \x -> ... `catch` \(_ :: SomeException) -> return (-1)
Namely catch all exceptions, and replace them with -1
. As usual with catch
, it is important to force evaluation of the ...
inside the catch
(e.g. using catchDeep
from safe-exceptions
). If you want to recover the original exception you can capture it in an IORef
and throw it after leaving C:
ref <- newIORef Nothing
f <- wrap $ \x -> ... `catch` \(e :: SomeException) -> do
writeIORef ref $ Just e
return (-1)
res <- apply f 22
whenJustM (readIORef ref) throwIO
However, what if there is an asynchronous exception after we leave the catch
but before we return to C? From my experiments, this doesn't appear to be possible. Even though getMaskingState
returns Unmasked
exceptions thrown to the function inside wrapper
appear to be deferred until the C code returns.
Suggestion: The documentation should clarify if my experiments are correct. Should getMaskingState
return MaskedUninterruptible
?
2 comments:
Thanks for the writeup! I had a question about "In real code you should always call freeHaskellFunPtr ...". What if the function pointer is a GUI callback, for example, an "onClick" function for some button widget written in Haskell which is invoked by the C/C++ event loop. Doesn't calling "freeHaskellFunPtr" free it after the first click causing a seg fault on subsequent callbacks?
@deech: I've tweaked the language to say free it after you have finished using it. With a GUI, you've never really finished using it (unless you tear down the window), so no need to free it as long as you only do the wrapper once.
Post a Comment