unsafePerformIO and FFI library initialization
Asked Answered
D

2

13

I'm creating an FFI module to a library in C which wants a 1-time, non-reentrant function to be called before anything else is. This call is idempotent, but stateful, so I could just call it in every Haskell call. But it's slow and due to non-reentrancy it could cause conflicts.

So is this the right time to use unsafePerformIO? I could wrap a Bool in an unsafe IORef or MVar to make these initialization calls idempotent by ignoring subsequent calls (calls where the global, hidden IORef state is False).

If not, what is the right way to do this?

Disunity answered 3/1, 2013 at 18:46 Comment(0)
C
11

I prefer the approach of initializing once and providing an unforgeable token as evidence that you have initialized the machine.

So your evidence would be:

data Token = Token

which you export abstractly.

Then your initialization function can return this evidence.

init :: IO Token

Now, you need to pass that proof to your API:

bar  :: Token -> IO Int
bar !tok = c_call_bar

etc.

You can now wrap this stuff up with a monad, or some higher order initialization environment to make it cleaner, but that's the basic idea.

The problem with initializing C libraries using hidden state is that you end up either not being able to parallelize access to the library, or having problems in GHCi, mixing compiled and bytecode, with two different versions of the C library loaded (which will fail with a linker error).

Catnap answered 3/1, 2013 at 19:2 Comment(7)
One alternative that has seen use is the withX wrapper around main. This gives no static guarentees, I'm just saying there's precedence (ex. withSocketsDo from the network package).Bra
Ah yes, good point. Simpler than the withToken $ \t ->, but no guarantees.Catnap
Ahh, excellent! This is a much better solution. I had been worried about how the global state would interact with multithreading (is an unsafe'd MVar thread local, runtime local?). This also makes initialization failure localizable in the Haskell runtime instead of just implicit and hidden.Disunity
I've been thinking about whether to "now wrap this stuff up with a monad". From one side, it looks conceptually nice: inside the new monad, you have actions of this special monadic type, which are correct to call only after the initialization. To interleave them with normal IO actions, I'd have to create a monad transformer for this (and lift the IO actions). But then I wondered: is this really worth it?! One could want a monad to order actions (different orders of modifying the initialized state give different results), but IO already imposes ordering. It erases the need for smart monads!Shakeup
...And next I want to understand: passing Tokens (representing the initialized state, a singleton object) looks like passing World instead of having an IO monad. And that is bad, because there are no guarantees against duplicating the world (non-linear uses; trying to use an old state once again). Ok, is that also bad for initialization Tokens? I think, no, because the things that need the initialized state are IO actions anyway, so they inherently are understood as linearly acting on any world state (including the initialized state under scrutiny). This is somehow dull (imperative)!..Shakeup
As for creating a monad ("now wrap this stuff up with a monad") for actions dependent on an idempotent (stateful) prior action: I've just read an idea which can be employed for a more general problem: spit out many createDirectory, then this list of actions is reduced with nub and fed into IO. In case when there is only one unparameterized thing you must initialize, you can avoid the overhead of nubShakeup
@DonStewart I took the liberty of removing your statement about DataKinds being able to rule out undefined evidence because it's not true. I hope you don't mind. If I've misunderstood you please let me know!Rodi
S
2

I'd like to note that currently some new trick is suggested for/instead of withSocketsDo by Neil Mitchell, based on evaluate ("Forces its argument to be evaluated to weak head normal form when the resultant IO action is executed."):

withSocketsDo act = do evaluate withSocketsInit; act 

{-# NOINLINE withSocketsInit #-}
withSocketsInit = unsafePerformIO $ do
    initWinsock
    termWinsock

My approach to removing the requirement to call withSocketsDo was to make it very cheap, then sprinkle it everywhere it might be needed.

Not necessarily this is a beautiful idea...

(See also his answer announcing this update in the library.)

Shakeup answered 26/2, 2015 at 21:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.