Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why isn't Box<T> FFI-safe for sized T? #334

Closed
jorendorff opened this issue May 4, 2022 · 4 comments
Closed

Why isn't Box<T> FFI-safe for sized T? #334

jorendorff opened this issue May 4, 2022 · 4 comments

Comments

@jorendorff
Copy link

Hi, apologies in advance if I'm asking this in the wrong place or year. ;)

My real use case: Define a big "everything-but-the-kitchen-sink"-style struct in Rust and pass it by pointer between Rust and C, using cbindgen to make the fields accessible from C, as part of adding Rust code to an existing C project. One of the fields is an Option<Box<OtherStruct>>. Rust warns that Box<T> is not FFI-safe.

Current workaround: Our own custom, FFI-safe box type (based on NonNull<T>).

The documentation for Box says:

So long as T: Sized, a Box<T> is guaranteed to be represented as a single pointer and is also ABI-compatible with C pointers (i.e. the C type T*).

How do we feel about that sentence? If we accept that, is there a good reason for Box<T> not to be FFI-safe when T: Sized?

@RalfJung
Copy link
Member

RalfJung commented May 4, 2022

This might be related to #198 -- if you declare a function as Box on the Rust side, then it effectively becomes restrict on the C side and all callers (even C-to-C-calls) need to respect that.

@jorendorff
Copy link
Author

By this reasoning,

  1. It's equally true that rustc should not consider Unique<T> and &mut T FFI-safe either, as far as this consideration goes, right? Those types have the same uniqueness restriction (but are considered FFI-safe now).

  2. For that matter, &T would also be affected; it'd translate as something like const T * restrict, right?

  3. By writing our own box based on NonNull<T> rather than Unique<T>, we may have unwittingly written something much safer to use from C, yes?

I don't understand your hypothetical. Why would a Rust declaration for a C-implemented function constrain C callers of that function? It seems sufficient for the purposes of a Rust spec to say what happens (and what behavior by this function will cause UB) when it's called from Rust. Who cares what happens when it's called from C?

(The other thing I don't understand is how literally to take "effectively becomes restrict"... restrict seems so underpowered and perversely specified; I won't enjoy trying to reason from that...)

@RalfJung
Copy link
Member

RalfJung commented May 7, 2022

I don't understand your hypothetical. Why would a Rust declaration for a C-implemented function constrain C callers of that function?

You end up having multiple different declarations of the same function (some with and some without noalias) -- technically that's UB, but in practice the worst that will happen is that it will pick some signature for all the calls.

For this to affect calls from C, xLTO needs to be enabled. Without xLTO it still affects the case where Rust code imports the same function multiple times with different signatures. If any signature makes aliasing guarantees, behavior is as if all signatures did that, and hence all callers need to be careful.

Indeed this also affects Unique (an unstable type, so not very relevant here) and &mut.

To be clear, I am not entirely sure if this is the reason for the FFI warning. I just wanted to mention this since this is a reason for being careful with using Rust reference types in FFI signatures.

By writing our own box based on NonNull rather than Unique, we may have unwittingly written something much safer to use from C, yes?

That box is definitely much safer in various ways, since it opts-out from all the Box guarantees (aliasing, dereferencability).

@jorendorff
Copy link
Author

jorendorff commented May 31, 2022

Ralf, thanks for explaining this. I was able to amaze my friends with this knowledge.

To sum up:

  • We're not sure if any of the following is really the answer to the question; I'd have to ask elsewhere. But a perhaps sufficient reason, one we do know, is:

  • Ideally, FFI-safe types should have the same valid values and safety rules in Rust that they have in C, not just the same size and alignment. This is indeed true of many FFI-safe types: the primitive types except bool, arrays of FFI-safe types, and pointers. It's not true of reference types, which are FFI-safe, and indeed that's less-than-ideal (but that ship has sailed).

  • Box<T> should not be considered FFI-safe, translating into C as T*, because it doesn't have the same valid values and safety rules as T*, even remotely, and even worse, some of those rules are actually quite subtle, thus tricky to follow from C. Aside from layout, the pointer must be dereferenceable, it must point to an allocation from the appropriate allocator, and it must be unique.

  • And LLVM knows that Rust Box<T> and C T* have different safety rules. It's reflected in function signatures (cf. the discussion above of restrict and noalias). So it's impossible to write a declaration in C that actually agrees with the signature of a Rust function taking a Box<T> (even declared extern "C"), and having different signatures for the same function is undefined behavior (and can actually be bad in practice).

Thanks again for your time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants