5
\$\begingroup\$

In Haskell, I want to apply a transformation to each element of a vector using the FFI. I am rather ignorant in C, I am more or less novice in Haskell and this is the first time I use storable vectors in Haskell.

This is what I did, taking the transformation \x -> x+1 as an example. The C file:

double* bar (int n, double *arr){
    double* y;
    y = (double*) calloc(n, sizeof(double)); 
    for(int i=0; i<n; i++)
      y[i] = arr[i]+1;
    return y;
}

The Haskell code:

import Foreign.C.Types
import Foreign (newForeignPtr)
import qualified Data.Vector.Storable as SV

foreign import ccall "bar" c_bar :: CInt -> Ptr CDouble -> Ptr CDouble

haskellBar :: SV.Vector CDouble -> IO (SV.Vector CDouble)
haskellBar sv = do
    let n = SV.length sv
    ptr <- SV.unsafeWith sv (return . (c_bar (fromIntegral n)))
    fptr <- newForeignPtr_ ptr
    return $ SV.unsafeFromForeignPtr0 fptr n

This works, but as I'm almost ignorant in C and new to storable vectors, I really don't know whether it's a good way and I would appreciate some advice.

I'm also new to the FFI by the way, and I don't know if I should write foreign import ccall safe/unsafe (I have not understood the role of safe/unsafe), or whether safe/unsafe is useless here.

\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

calloc can fail

We start with the C code first, since it's somewhat shorter. Aside from the name bar, we see one problem: calloc might return 0. Therefore, we should check that:

double* bar(int n, double *arr){
    double* y;
    y = (double*) calloc(n, sizeof(double)); 

    if(!y) {
      return 0;
    }

    for(int i=0; i<n; i++){
      y[i] = arr[i]+1;
    }
    return y;
}

Who frees the memory?

However, there is still one problem. A user needs to know how add_one conjured that memory in order to use the correct free variant. That's something that can be handled with documentation. Still, there is no need for us to call calloc, we can just give the responsibility to the user:

double* add_one(double * dest, const double * src, size_t count){
    for(size_t i = 0; i < count; ++i) {
      dest[i] = src[i] + 1;
    }
    return dest;
}

Note that this uses const to make sure that we don't overwrite src by accident. We also use size_t to be ready for any kind of size the user might throw at us. The user is now responsible to allocate the memory, so they will also know how to free it.

We return the pointer because we followed memcpy's interface, but we could also just make it a void function instead.

Haskell part

Let's go to Haskell. We can see that there is something off: you import newForeignPtr, but you use newForeignPtr_. That's probably since newForeignPtr needs the aforementioned free function. That's in Foreign.Marshal.Alloc:

fptr <- newForeignPtr finalizerFree ptr

But before we delve into that, let us take a step back and look at the foreign import:

foreign import ccall "bar" c_bar :: CInt -> Ptr CDouble -> Ptr CDouble

Unfortunately, your c_bar does not have that type. Remember, whenever you see A -> B in a function, you should always get the same B if you've used the same A:

all (== foo a) [foo a,foo a,foo a,foo a,foo a] -- all the same

But that does not hold for your function. It has a side-effect: every call will return another Ptr CDouble. We have to put it in IO:

foreign import ccall "bar" c_bar :: CInt -> Ptr CDouble -> IO (Ptr CDouble)

Note that you can use the unsafe keyword here. It basically means that bar will never call any Haskell function and will stay in its C world. With this, we would end up with

haskellBar :: SV.Vector CDouble -> IO (SV.Vector CDouble)
haskellBar sv = do
    let n = SV.length sv
    ptr <- SV.unsafeWith sv (c_bar (fromIntegral n))
    fptr <- newForeignPtr finalizerFree ptr
    return $ SV.unsafeFromForeignPtr0 fptr n

Which does not differ from your variant too much.

The add_one variant is similar:

foreign import ccall unsafe "add_one" c_addOne :: Ptr CDouble -> Ptr CDouble -> CSize -> IO (Ptr CDouble)

It's in IO, it uses unsafe to be a little bit faster (but must not call Haskell from C), and it uses the correct types. It's wrapper is almost the same, but this time, we allocate the memory in Haskell:

addOne :: SV.Vector CDouble -> IO (SV.Vector CDouble)
addOne sv = do
   fptr <- mallocForeignPtrArray n
   SV.unsafeWith sv $ \v -> withForeignPtr fptr $ \f -> c_addOne f v (fromIntegral n)
   return $ SV.unsafeFromForeignPtr0 fptr n
 where
   n = SV.length sv

The mallocForeignPtrArray makes sure that the memory gets freed correctly.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.