Box over FFI boundary


#1

I was wondering if Box is safe to expose in FFI signatures. Box is not marked as repr(C) but its layout seems to be compatible. Consider for example the following C-API:

#ifndef TEST_H
#define TEST_H

#include <stddef.h>
#include <stdint.h>

typedef struct vector_t vector_t;

void   new(vector_t **);
void   del(vector_t *);
void   add(uint8_t, vector_t *);
size_t len(vector_t const *);

#endif

Would this Rust implementation be safe?

use std::ptr;

#[no_mangle]
pub extern fn new(x: *mut Box<Vec<u8>>) {
    let v = Box::new(Vec::new());
    unsafe {
        ptr::write(x, v)
    }
}

#[no_mangle]
pub extern fn del(_: Box<Vec<u8>>) {}

#[no_mangle]
pub extern fn add(x: u8, v: &mut Vec<u8>) {
    v.push(x)
}

#[no_mangle]
pub extern fn len(v: &Vec<u8>) -> usize {
    v.len()
}

It certainly compiles and links fine with a little test program:

#include <assert.h>
#include <test.h>

int main () {
        vector_t * v = NULL;
        new(&v);
        add(1, v);
        add(2, v);
        assert(2 == len(v));
        del(v);
}

Am I relying on some unsafe behaviour here or would this be a correct approach to write a Rust FFI?


#2

Although Box's representation is currently identical to that of a raw pointer, it’s more future-proof to use from_raw and into_raw (that are stable in Rust 1.4 coming out in two weeks).

#[no_mangle]
pub extern fn new(x: *mut *mut Vec<u8>) {
    unsafe {
        let v = Box::new(Vec::new());
        *x = Box::into_raw(v);
    }
}

#[no_mangle]
pub extern fn del(v: *mut Vec<u8>) {
    unsafe {
        Box::from_raw(v);
    }
}

#3

If it’s not repr(c), then there’s no guarantees about its representation, at all.


#4

I wonder, is improper_ctypes lint supposed to be smart enough to warn about x: Box<Vec<Foo>> (it doesn’t do even that so far) but allow x: *mut Vec<Foo> (although you still can do *x = Vec::new())? Or should it deny any mentions of non-repr(C) types no matter the level of indirection?


#5

Box itself shouldn’t be used in FFI, but as @gkoz points out, it can be converted to a raw pointer. I find it useful in C APIs that take an untyped pointer as a user-provided value together with a function pointer to call (with the data pointer as argument) when the value stops being used. You can pass an extern "C" fn that converts the pointer back to the Box and drops it.