Preparing an array of structs for FFI

I'm working on a Rust library which will be called from C++.
Currently filling out the FFI layers. I need to pass an array of structs, which I've prepared for cbindgen to create my bindings:

#[repr(C)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SerialPortTypeFFI {
    UsbPort,
    PciPort,
    BluetoothPort,
    Unknown,
}

#[repr(C)]
#[derive(Copy, Clone)]
pub struct SerialPortInfoFFI {
    pub port_name: *const c_char,
    pub port_type: SerialPortTypeFFI
}

I want to turn a Vec<SerialPortInfoFFI> into an array or vector that can be processed on the C++ side. Do I need to pass it as a void* (*const c_void) and cast it or is there a way to pass it as an array? The C version would be e.g. struct SerialPortInfoFFI* port_data

Rust slices expose both a pointer to the first element (if any), and a len getter.
These two elements are supposed to be used together for FFI, by letting you manipulate "C slices" (thinggy * pointer + size_t count).

You can create a convenience struct to hold both fields, as would be done in C, but one thing to be careful with is ownership: since a Vec is allocated by Rust, you will need to give the owned slice back to Rust so that it can properly free it:

use ::core::{ptr, slice};

#[repr(C)]
pub
struct FFIBoxedSlice {
    ptr: *mut SerialPortInfoFFI,
    len: usize, // number of elems
}

// Helper (internal) function
fn vec_to_ffi (v: Vec<SerialPortInfoFFI>)
  -> FFIBoxedSlice
{
    // Going from Vec<_> to Box<[_]> just drops the (extra) `capacity`
    let boxed_slice: Box<[SerialPortInfoFFI]> = v.into_boxed_slice();
    let len = boxed_slice.len();
    let fat_ptr: *mut [SerialPortInfoFFI] =
        Box::into_raw(boxed_slice)
    ;
    let slim_ptr: *mut SerialPortInfoFFI = fat_ptr as _;
    FFIBoxedSlice { ptr: slim_ptr, len }
}

#[no_mangle]
pub
unsafe
extern "C"
fn free_boxed_slice (FFIBoxedSlice { ptr, len }: FFIBoxedSlice)
{
    if ptr.is_null() {
        eprintln!("free_boxed_slice() errored: got NULL ptr!");
        ::std::process::abort();
    }
    let slice: &mut [SerialPortInfoFFI] =
        slice::from_raw_parts_mut(ptr, len)
    ;
    drop(Box::from_raw(slice));
}
1 Like

Thank you for the detailed reply and the example code.

Since the calling code will be C++, I'm wondering if I can do the "freeing" part on the C++ side, since it makes sense for it to take ownership of the array, E.g using std::unique_ptr, or even (god forbid) delete?

Actually, I would use the slice (ptr+len) to initialise a std::vector in C++, which I assume would then manage the lifetime of the memory.

In which case, would I need to mem::forget(v) at the end of vec_to_ffi? In this case, I think it should create a copy of the Vec first...

EDIT Looks like into_boxed_slice calls mem::forget on itself.

UPDATE

I can't get the "free" function to pass cbindgen: I get

ERROR: Cannot use fn mylib::free_serial_port_info_slice (Parameter has an unsupported type.).

You can look at my project https://github.com/dushistov/rust_swig that can generate bindings for several languages including C++. It generate almost as @Yandros desbribed, plust C++ class that hide these C API and call analog of free_boxed_slice in destructor.

2 Likes

You cannot really free it on C++ side, be it by directly using delete or by indirectly using it through an owning abstraction such as unique_ptr, unless you know that both C++ and Rust are using the exact same allocator and in the same fashion. Hence the more careful approach of "giving the thing back to Rust so that it can free it".

Now, you can perfectly write C++ abstractions around Rust allocations; for instance, you can have you own class that contains a FFIBoxedSlice and whose destructor calls free_boxed_slice

Hmm, the error may be caused by my destructuring pattern-match within the very argument of the function; what happens if you change the function prototype to:

#[no_mangle]
pub
unsafe
extern "C"
fn free_boxed_slice (arg: FFIBoxedSlice)
{
    let FFIBoxedSlice { ptr, len } = arg;

?

1 Like

Aha, thanks yes, the destructuring was the issue with cbindgen.
Good tip about abstracting the allocation and free into a C++ class.