Data-layout over FFI

Hello,

I'm working on FFI situation where the C side is:

typedef void (*foo)(void*);

void exec(foo f, void *i) {
  return f(i);
}

and Rust side looks like:

extern "C" {
    pub fn exec(f: Option<unsafe extern "C" fn(f: *const std::ffi::c_void)>,
                i: *const std::ffi::c_void);
}

#[derive(Default)]
struct Foo {
    // whatever
}

extern "C" fn use_foo(f: *const std::ffi::c_void) {
    let f = unsafe { &*(f as *const Foo) };
    // use Foo...
}

fn main() {
    let foo: Foo = Default::default(); // some Foo
    unsafe { exec(Some(use_foo), &foo as *const _ as _) };
}

Since the pointer dereference &*(f as *const Foo) is done on a f (as a valid Foo reference), I believe that calling the C function exec from Rust side in this case is always safe, for any data-layout of Foo.

Is this true?

always is ambiguous: the pattern used in main is indeed always safe, provided Foo is Sized.


However, both use_foo and exec are not "always safe", on the contrary, both require that f be a valid reference to a Foo.

That's why, for instance, your use_foo function should be declared unsafe.


Finally, as a safeguard, instead of

unsafe
extern "C"
fn use_foo (f: *const ::std::ffi::c_void)
{
    let f: &Foo = &*(f as *const Foo);
    // use Foo...
}

you should use either:

unsafe
extern "C"
fn use_foo (f: *const ::std::ffi::c_void)
{
    // Check against NULL input
    let f: &Foo =
        (f as *const Foo)
            .as_ref()
            .unwrap_or_else(|| ::std::process::abort()) 
    // use Foo...
}

or, if if you know you will be using that pattern anyways:

extern "C"
fn use_foo (f: Option<&'_ Foo>)
where
    Foo : Sized,
{
    // Check against NULL input
    let f: &Foo = f.unwrap_or_else(|| ::std::process::abort()) 
    // use Foo...
}
  • This has the advantage of no longer requiring the unsafe annotation, since Rust will then only be able to call it with None or Some valid reference to a Foo :slight_smile:

  • but, for your example, this will give use_foo a different function pointer type than the one expected by exec, so you should use the previous pattern instead.

3 Likes

@Yandros Thank you for a fantastic answer, as always.

Sorry for not being clear, but what you've guessed is indeed what I want to mean.

I was not aware of your second pattern but it's indeed very nice, and I think it's applicable in this context because a

extern "C" fn use_foo(f: Option<&'_ Foo>);

should be ABI compatible with

extern "C" use_foo(f: *const std::ffi::c_void)

I can only think of a case where they may not compatible is where the alignments of f are not the same, does another case exist?

1 Like