How to wrap unsafe FFI? (Haskell)

275 views Asked by At

This is a followup question to Is there ever a good reason to use unsafePerformIO?

So we know that

p_sin(double *p) { return sin(*p); }

is unsafe, and cannot be used with unsafePerformIO.

But the p_sin function is still a mathematical function, the fact that it was implemented in an unsafe way is an implementation detail. We don't exactly want, say, matrix multiplication to be in IO just because it involves allocating temporary memory.

How can we wrap this function in a safe way? Do we need to lock, allocate memory ourselves, etc? Is there a guide/tutorial for dealing with this?

1

There are 1 answers

3
luqui On BEST ANSWER

Actually, if you incorporate the way p_sin is unsafe from that answer, it depends on p_sin not being a mathematical function, at least not one from numbers to numbers -- it depends on giving different answers when the memory the same pointer points to is different. So, mathematically speaking, there is something different between the two calls; with a formal model of pointers we might be able to tell. E.g.

type Ptr = Int
type Heap = [Double]

p_sin :: Heap -> Ptr -> Double

and then the C function would be equivalent to

p_sin h p = sin (h !! p)

The reason the results would differ is because of a different Heap argument, which is unnamed but implicit in the C definition.

If p_sin used temporary memory internally, but did not depend on the state of memory through its interface, e.g.

double p_sin(double x) {
    double* y = (double*)malloc(sizeof(double));
    *y = sin(x);
    x = *y;
    free(y);
    return x;
}

then we do have an actual mathematical function Double -> Double, and we can

foreign import ccall safe "p_sin" 
    p_sin :: Double -> Double

and we're be fine. Pointers in the interface are killing the purity here, not C functions.

More practically, let's say you have a C matrix multiplication function implemented with pointers, since that's how you model arrays in C. In this case you'd probably expand the abstraction boundary, so there would be a few unsafe things going on in your program, but they would all be hidden from the module user. In this case, I recommend annotating everything unsafe with IO in your implementation, and then unsafePerformIOing right before you give it to the module user. This minimizes the surface area of impurity.

module Matrix
    -- only export things guaranteed to interact together purely
    (Matrix, makeMatrix, multMatrix) 
where

newtype Matrix = Matrix (Ptr Double)

makeMatrix :: [[Double]] -> Matrix
makeMatrix = unsafePerformIO $ ...

foreign import ccall safe "multMatrix" 
   multMatrix_ :: Ptr Double -> IO (Ptr Double)

multMatrix :: Matrix -> Matrix
multMatrix (Matrix p) = unsafePerformIO $ multMatrix_ p

etc.