Option is FFI safe or not

Hello all, I have a new question about FFI.

Given a C function which return a null-terminated string or null:

char *foo();

I think that the Rust signature with Option<NonNull<u8> instead of raw pointer *const u8

extern "C" {
  fn foo() -> Option<NonNull<u8>>;
}

should be safe because of nullable pointer optimization.

But the compiler still shows the warning:

warning: extern block uses type std::option::Option<std::ptr::NonNull<u8>> which is not FFI-safe: enum has no representation hint

Is this signature “really” safe or there are cases where it isn’t safe?

Many thanks for help.

Hmm, the error says enum has no representation hint so have you tried wrapping your return value in a struct and setting an repr? Perhaps something like this:

#[repr(C)]
struct MyOption(Option<NonNull<u8>>);

Please, just stick to the correct C function (or use bindgen which does it for you) and then use a wrapper which converts the pointer to an Option, if you want.
Else it may cause a very hard to debug error, which can cost you a lot of time.

2 Likes

@hellow and @chrisd: thank you for the suggestions (and again @hellow for another response).

It may be wise to just follow things people have done but I still want to understand why people do this and not do that. Because I think that there is a conflict in my understanding about Option over FFI, I just want to know which side is not correct.

Option::None is not guaranteed to be 0, therefore you cannot simply convert a pointer to an Option and hope it will be that.

fn f(o: Option<bool>) {
    unsafe { dbg!(std::mem::transmute::<_, u8>(o)) };
}
fn main() {
    f(Some(false)); // 0
    f(Some(true));  // 1
    f(None);        // 2
}

Therefore Option is not FFI safe and should not be used (ultimatly because Rust ABI is not stable (nor really specificied) it is not FFI safe. (Even C++s ABI is not specified. IIRC only C has a stable ABI, and that’s why it is the lingua franca).

2 Likes

As I understand it, the compiler is (in principle) free to optimize your types in whatever way it likes so long as it doesn’t break Rust code. So it may have one memory layout today but that could change in a future version.

When you pass it over FFI you need to tell the compiler to use an explicit memory layout so that it can’t optimize the type in ways that break foreign functions.

The easiest way to do this is to use primitive types with a known layout. You can then have a wrapper function that converts to Rust types. Which might be a no-op anyway after its optimized.

Option::None is not guaranteed to be 0

This is correct in general but in the context of nullable pointer optimization, it would be guaranteed. The following phrase is from nomicon

If T is an FFI-safe non-nullable pointer type, Option<T> is guaranteed to have the same layout and ABI as T and is therefore also FFI-safe

Even more, if I use an Option type myself:

#[repr(C)]
pub enum Option<T> {
  None,
  Some<T>,
}

the compiler will be happy, no more warning.

2 Likes

In this specific scenario, there is a std::ffi::CStr for dealing with C strings which has a from_ptr constructor that takes a *const c_char. You really should write a wrapper function that returns a &CStr here.

I suppose that for “nullable pointer optimization” Option parameter should be representable in C,
May be if mark NonNull with #[repr(transparent)] from this RFC https://github.com/rust-lang/rust/issues/60405 things become different?

2 Likes

@Dushistov, thank you.

I think this is the problem, because for nullable pointer optimization, it’s required that T is a type which is never null, but more concretely

Certain Rust types are defined to never be null . This includes references ( &T , &mut T ), boxes ( Box<T> ), and function pointers ( extern "abi" fn() )… However, the language provides a workaround.
… an enum is eligible for the “nullable pointer optimization” if it contains exactly two variants, one of which contains no data and the other contains a field of one of the non-nullable types listed above.

Though NonNull<T> is never null, it is not included in these types (i.e. &T, &mut T, …) then the compiler gives warning about that. I can confirm that the following:

extern "C" {
  fn foo() -> Option<&'static u8>;
}

makes the compiler happy.

2 Likes

Yes, I stumbled upon this warning about Option<NonNull<T>> not being FFI-safe a few days ago, and I concur that it is weird and inconsistent.

When T : Sized, NonNull<T> is guaranteed to have one niche: the null bit pattern (0_usize). Therefore enum layout optimization makes Option<NonNull<T>> be guaranteed to map None to this null bit pattern, and Some(x) to x.

Given the current warning, and while waiting for it to be fixed, you can disable the lint on that signature (#[allow(improper_ctypes)]), and have the following guard around:

extern "C" {
    #[allow(improper_ctypes)]
    fn foo () -> Option<NonNull<u8>>;
}

/// Checks that Option<NonNull<u8>> and usize have the same size,
/// and that None is null. 
#[deny(const_err)]
const _GUARD: () = [()][unsafe {
    type Src = Option<NonNull<u8>>;
    type Dst = usize;

    let _: [();
        ::core::mem::size_of::<Src>()
    ] = [();
        ::core::mem::size_of::<Dst>()
    ];

    union Transmute {
        src: Src,
        dst: Dst,
    }

    Transmute { src: None }.dst
}];
2 Likes