haskellffiunsafe-perform-io

How to wrap unsafe FFI? (Haskell)


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?


Solution

  • 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.