Is this FFI sound?

I've created an FFI to interact with an external library. I think it's sound, but I'd love to get some more eyes on it, and more crucially, find out how I can verify that it isn't leaking memory without resorting to Valgrind, as I'm using macOS.

use std::f64;
use std::{mem, slice};

/// A C-compatible `struct` originating **outside** Rust
/// used for passing arrays across the FFI boundary
#[repr(C)]
pub struct ExternalArray {
    pub data: *const libc::c_void,
    pub len: libc::size_t,
}

/// A C-compatible `struct` originating **inside** Rust
/// used for passing arrays across the FFI boundary
#[repr(C)]
pub struct InternalArray {
    pub data: *const libc::c_void,
    pub len: libc::size_t,
}

impl From<Vec<[f64; 2]>> for InternalArray {
    fn from(sl: Vec<[f64; 2]>) -> Self {
        let mut v = sl;
        v.shrink_to_fit(); // <-- this doesn't do what I thought it does
        let array = InternalArray {
            data: v.as_ptr() as *const libc::c_void,
            len: v.len() as libc::size_t,
        };
        mem::forget(v);
        array
    }
}

impl Drop for InternalArray {
    fn drop(&mut self) {
        if self.data.is_null() {
            return;
        } else {
            let _: Vec<[f64; 2]> =
                unsafe { Vec::from_raw_parts(self.data as *mut [f64; 2], self.len, self.len) };
        }
    }
}


impl From<ExternalArray> for Vec<[f64; 2]> {
    fn from(arr: ExternalArray) -> Self {
        unsafe { slice::from_raw_parts(arr.data as *mut [f64; 2], arr.len).to_vec() }
    }
}


impl From<InternalArray> for Vec<[f64; 2]> {
    fn from(arr: InternalArray) -> Self {
        // because this data originated in Rust we don't need to go via a slice in order to take ownership
        unsafe { Vec::from_raw_parts(arr.data as *mut [f64; 2], arr.len, arr.len) }
    }
}

The code is hopefully self-explanatory, but to explain its flow:

  1. The code is called by an external process which passes an ExternalArray to Rust;
  2. Because this data is to be mutated in Rust, I have to take ownership of it;
  3. When I'm ready to return the result to the external caller I convert the Vec<[f64; 2]> to an ExternalArray, mem::forget the Vec, and pass the ExternalArray back across the FFI;
  4. When the external caller is finished with it, it's passed back a final time via drop_internal_array;

Another problem I may have is that the shrink_to_fit call isn't actually guaranteeing what I think it is. Is there a better way of accomplishing this?

I would prefer that you use Vec::as_mut_ptr over Vec::as_ptr because the latter exposes an immutable-only pointer.

The caller must also ensure that the memory the pointer (non-transitively) points to is never written to (except inside an UnsafeCell ) using this pointer or any pointer derived from it. If you need to mutate the contents of the slice, use as_mut_ptr .

Anyway, for something like this I recommend that you use a Box<[[f64; 2]]> instead of Vec<[f64; 2]>. You can make such a box from a vector with into_boxed_slice. Once you have a Box you should use Box::into_raw and a pointer to cast to create your InternalArray, and to drop it, use slice_from_raw_parts_mut followed by Box::from_raw.

3 Likes

That's really helpful. So something like

impl From<Vec<[f64; 2]>> for InternalArray {
    fn from(sl: Vec<[f64; 2]>) -> Self {
        let boxed = sl.into_boxed_slice();
        let blen = boxed.len();
        let rawp = Box::into_raw(boxed);
        let array = InternalArray {
            data: rawp as *mut libc::c_void,
            len: blen as libc::size_t,
        };
        array
    }
}

to create the InternalArray, and

impl Drop for InternalArray {
    fn drop(&mut self) {
        if self.data.is_null() {
            return;
        } else {
            let _ = unsafe {
                let p = ptr::slice_from_raw_parts_mut(self.data as *mut [f64; 2], self.len);
                Box::from_raw(p)
            };
        }
    }
}

to drop it?

1 Like

Yes, that seems ok. A personal preference is to write it like this:

unsafe {
    let p = ptr::slice_from_raw_parts_mut(self.data as *mut [f64; 2], self.len);
    drop(Box::from_raw(p));
}

Here it is explicit that we are dropping the box.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.