Pre-Pre-RFC: FFISafe trait

Summary

Add a new FFISafe trait that is implemented exclusively for FFI-safe types.

Motivation

In an earlier variadics PR, I was trying to implement VaList::arg::<T> for the set of all FFI-safe types, i.e., the types allowed by the improper_ctypes lint, and exclude FFI-unsafe types like fat pointers. The issue I ran into is that the lint runs before monomorphization and does not handle type parameters, so it would never complain about using VaList::arg on a FFI-unsafe type.

One solution would be for the compiler and libcore to provide a FFISafe trait implemented exclusively for all FFI-safe types. If that were available, VaList::arg could be implemented as arg<T: FFISafe>.

Guide-level explanation

libcore would provide a FFISafe auto trait which is implemented for the following types (same as the types supported by improper_ctypes):

  • FFI-safe primitive types: bool, integers, floats, never-type
  • Pointers, references and arrays of FFI-safe types
  • FFI-safe function pointers
  • Foreign types (more specifically, pointers to foreign types are FFI-safe thin pointers)
  • #[repr(C)] or transparent structures, unions and enums containing only FFI-safe fields (with a few edge cases)

Reference-level explanation

Since FFISafe is implemented for foreign types and has a few additional restrictions, e.g., checking for #[repr(C)], it requires compiler support. The trait would be automatically implemented by the compiler in the same way as Copy and Sized.

Drawbacks

Other than implementing VaList::arg, it's unclear what other uses this could have. I wrote this pre-RFC to get some feedback on this, and see if anyone else could use it.

1 Like

I'm skeptical that we should be adding a standard market trait primarily because we ran into difficulty getting the improper_ctypes lint to work on VaList.

Perhaps more importantly, I'm pretty sure there's no single clear definition of "FFI-safe", because what that means depends on what the foreign code is or does. An example I recently saw come up elsewhere (but I forget where) is that a raw pointer to any type is safe in one sense if the foreign code never dereferences it.

Can we not simply teach the relevant lints to do something special for VaList? After all, VaList is a special construct which has no other purpose than making FFI with variadic C code possible, so having other things hardcode knowledge of it seems entirely appropriate.

2 Likes

Can we not simply teach the relevant lints to do something special for VaList? After all, VaList is a special construct which has no other purpose than making FFI with variadic C code possible, so having other things hardcode knowledge of it seems entirely appropriate.

Sure, I'm just not sure how that would work. The problem is that VaList::arg is generic over the type of that argument, and the lint couldn't handle that. I looked into that a while ago and my initial idea was to have the lint check all direct VaList::arg call sites to make sure that the T is FFI-safe in those places, but it can't be guaranteed that T is not itself another type parameter in those places. Consider something like this:

fn arg2<T>(mut ap: VaList) -> (u32, T) {
  let x = ap.arg();
  let y = ap.arg::<T>();
  (x, y)
}

The lint would also have to check that arg2 is only called for FFI-safe types.

I would've thought it's acceptable for a lint to "give up" on something that generic (after all, it's a lint).

Is this some kind of extremely common C variadics usage pattern I'm not familiar with?

Is this some kind of extremely common C variadics usage pattern I'm not familiar with?

Not at all, it's just an extreme example of FFI unsafety that would get past the lint. In most practical cases, you'd be calling arg directly with the type being known at the call site (if the lint runs after type inference, which it does).

That would be https://github.com/rust-lang/rust/issues/66220

I agree that a single trait denoting "FFI safe" is too overloaded. If the intent is "safe to pass in varargs" then the trait should capture that and be named and explained accordingly, especially if nobody has any other uses in mind for the trait.

1 Like

Isn't "StableABI" a better name for this concept?

That is, something is "StableABI" if we promise that the memory layout will always be the same with any Rust compiler and compilation setting (that doesn't explicitly change the ABI).

For instance, raw pointers to any type should indeed be StableABI, just like the "char" type, and so on.

1 Like

Isn't "StableABI" a better name for this concept?

There might be Rust-specific types in the future with a stable ABI, and it's unclear to me if those should be considered "FFI-safe". For this trait, what I had in mind was the exact set of types allowed by improper_ctypes, so maybe the name should be ProperCType or CABIType.

If the intent is "safe to pass in varargs" then the trait should capture that and be named and explained accordingly, especially if nobody has any other uses in mind for the trait.

Finding out if anyone has other uses for this trait was basically the reason I wrote this. As far as fixing the original arg interface, there are other ways to do that, like extending the existing lint or using the Thin trait from RFC 2580.