Return Result<(), NonZeroI32> via FFI

I want a rust ffi function declared to return Result<(),NonZeroI32> to return it as a C uint32_t.

Will Result<(),NonZeroI32> do this? It compiles without severe warnings but I'm not sure what it will do cross-platform

Do I need repr(c) or repr(transparent) hacks for this, maybe on a new type declaration?

The docs NonZeroU32 in std::num - Rust say:Option<NonZeroU32> is the same size as u32 so I'm hoping Result can do the same and that () will map to zero.

Maybe there are some const assertions I should use?

The documentation is referring to a layout guarantee made specifically by Option. There is no analogous guarantee made by Result.

You will have to use Option<NonZeroU32> or a transparent wrapper around it — there is no other way to get that particular layout guarantee, because there is no repr for "must use a zero niche".

3 Likes

No. First of all, the layout of #[repr(Rust)] types (including any type without an explicit #[repr]!) is unspecified unless there is an explicit guarantee.

Furthermore, and more importantly, you can only ever send through the FFI boundary types that C itself knows about, which limits the set of FFI-safe types to primitives (integers, floats, and bool), #[repr(C)] structs constructed of them, arrays constructed of them, and pointers to them (and an arbitrarily deep combination/tree thereof). Real, algebraic enums with associated data are simply not a thing in C, therefore by default they are definitely not FFI-safe.

Now of course, there are enums with no associated data, which can be made FFI-safe when given the right #[repr], because they are then equivalent with C's dumb enums. Furthermore, there is Option, which is guaranteed to be FFI-safe when None has a niche, but that's pretty much a special-case guarantee of Rust regarding the C-compatibility of the layout of Option, and 1. it won't be represented as an enum on the C side (obviously), 2. you can't just assume that the same thing is automatically true for any other Rust enum. (However, there are plans to improve on that situation.)

AFAIK it doesn't help – you can't (currently) sensibly apply #[repr(C)] in order to make something inherently non-FFI-safe (like an enum with associated values) into something that is.

2 Likes

AFAIK it's safe to send anything with a compatible ABI, even if it's a fancy Rusty type. For example, thin Option<Box<T>> is ABI-compatible with *mut T, and can be used as-is in FFI. I take advantage of that to avoid Box::into_raw boilerplate.

4 Likes

Thanks. I'm in danger of blowing my own fingertips off with the wizardry here.

I guess I am going to have to have some adaptor code to convert from internal Result<(), NonZeroI32> to external Option<NonZeroI32>

I'm guessing result.err() is my best tool to convert from Result to the ffi-safe Some that my function must use.

But I think there is no way to avoid a line of code, which means an extra layer of functions, even if it's just an internal closure, so as to map from one to the other while preserving the option to use ?

The arguments are like: bucket: Option<NonNull<libc::c_char>>,
and the code I want to be able to use is:

        let bucket = bucket
            .map(|str| std::ffi::CStr::from_ptr(str.as_ptr()).to_str().unwrap())
            .ok_or(SW_ERR_INVALID_RQST)?;

so it must be enclosed in a Result<> function

Yes, if you want to use ? you will have to write a wrapper function. However, that function can be a closure:

use std::num::NonZeroU32;
use std::ptr::NonNull;

const SW_ERR_INVALID_RQST: NonZeroU32
    = unsafe { NonZeroU32::new_unchecked(1) };

pub unsafe fn ffi_exposed_function(
    bucket: Option<NonNull<libc::c_char>>,
) -> Option<NonZeroU32/> {
    (|| {
        let bucket = bucket
            .map(|str| std::ffi::CStr::from_ptr(str.as_ptr()).to_str().unwrap())
            .ok_or(SW_ERR_INVALID_RQST)?;
        Ok(())
    })()
    .err()
}

In the future Rust may also have try blocks which can replace this use of a closure, or the Try trait which would allow you to define ? behavior for a type you write that is a wrapper around Option<NonZeroU32>.

1 Like

But isn't that because it's yet another special guarantee around Box? It's not even transparent (even though its guts, Unique<T>, NonNull<T> are), and it contains another, possibly non-zero sized field (the allocator). That doesn't seem like it should be guaranteed to be, or even happen to be, compatible with *mut T without magic.

Doing this is fine as per the docs. As far as I'm aware, it is guaranteed because the standard library is allowed to depend on implementation details of the compiler in certain situations, rather than straight-up magic.

Regardless, I still wouldn't recommend passing boxes as ffi parameters, as it is pretty easy to cause abi mismatches with unsized types or by using a non zst allocator.

That example is so amazingly close to what I did!

Except I didn't realise I didn't have to specify the closure return type, and I couldn't find a way to declare the nonzero const because of unstable const feature, so thank you for those great tips!

I'm aware and agree; what I'm debating here is why this is. Indeed the fact that it's explicitly mentioned in the docs leads me to believe that it's not a universal property of every type. ("Std is allowed to rely on implementation details of the compiler" still counts as "magic", because no other library has that power.)

1 Like

Only a few Rust types have a defined ABI. It's not a general property. But there are more such types than just C equivalents.