Change:
The function itself is considered FFI-safe and does not trigger the warning (). Its function pointer should also be allowed, to be consistent. Discovered on CI The original code has several more levels of indirection, but is also considered FFI-safe before. <details<summaryExtracted original code</summary</detailsAccording to CI logs, the warning occurs since rust 1.72.0-nightly ( 2023-07-03). Probably caused by . cc
Okay, so, I had a fix for this that just got rid of the regression, but I'm not sure that's the correct solution here - I think this is correct to be linting in the fn-ptr case and should be linting for the function case. is FFI-unsafe, but we allow a function to return because that's the default, but that doesn't mean that a within a type is okay (with the exception of a wrapper around the - because that's just returning , which we've already decided is okay). In the function case that works right now, that's being allowed because of this code.. ..which is there to support the case. However, it's not quite the right check. It doesn't actually check for ! It checks that the type - however nested - which was deemed FFI-unsafe is and that type is part of a return type. So any FFI-unsafe in a return type makes the entire return type FFI-safe - which is clearly incorrect. That's making the example below compile, which absolutely shouldn't. I've submitted , which makes this even more of a regression by making the function case fail too. We already had a simple "is it a unit" check, so I extended that to check for transparent newtype wrappers too, and removed the incorrect check.
This is unfortunate since I'm trying to return a where is likely to be . What should I do instead if I want to be FFI-safe with layout ?
I'm not so sure anymore after reading comment (). It's clear that some changes are required regardless - our current implementation wrong as demonstrated in the final example from my previous comment, but it's also inconsistent, in that this example... lint for the argument use of , but not the return use of , when really it should be both or neither. After my pull request, it's both. With and without my pull request, when you change this to , it doesn't lint.
I'll nominate this issue so that it sees some discussion - see also and .
I posted some more info on zulip but it may also worth mentioning here. ZST is still zero-sized under repr(C), rather than FFI-unsafe.
The issue now also exists in beta. I also found uitests which seems to expect ZST to be FFI-safe, but ends in to suppress the warning and tests nothing.
We discussed this in today's meeting. We didn't have consensus that this should warn; at least some people thought the code in the top comment should continue to compile without warnings. We did have consensus that changing that (to warn) would need an FCP. So, we felt that we'd like to see a fix removing the warning applied to beta and nightly, and then separately if someone wants to propose that we change this to warn, we could evaluate that and do an FCP on it. But at the moment the temperature of the team seems to be that we don't have consensus for such an FCP.
WG-prioritization assigning priority (). label -I-prioritize +P-medium
labels -I-lang-nominated We rendered our opinion

Discussion:
lint_improper_ctypes_char_reason = the `char` type has no C equivalent lint_improper_ctypes_dyn = trait objects have no C equivalent <del> lint_improper_ctypes_enum_phantomdata = this enum contains a PhantomData field </del> lint_improper_ctypes_enum_repr_help = consider adding a `#[repr(C)]`, `#[repr(transparent)]`, or integer `#[repr(...)]` attribute to this enum

) -> FfiResult<'tcx> { use FfiResult::*; <del> let transparent_safety = def.repr().transparent().then(|| { // Can assume that at most one field is not a ZST, so only check // that field's type for FFI-safety. </del> <ins> let transparent_with_all_zst_fields = if def.repr().transparent() { </ins> if let Some(field) = transparent_newtype_field(self.cx.tcx, variant) { <del> return self.check_field_type_for_ffi(cache, field, args); </del> <ins> // Transparent newtypes have at most one non-ZST field which needs to be checked.. match self.check_field_type_for_ffi(cache, field, args) { FfiUnsafe { ty, .. } if ty.is_unit() => (), r => return r, } false </ins> } else { <del> // All fields are ZSTs; this means that the type should behave // like (), which is FFI-unsafe... except if all fields are PhantomData, // which is tested for below FfiUnsafe { ty, reason: fluent::lint_improper_ctypes_struct_zst, help: None } </del> <ins> // ..or have only ZST fields, which is FFI-unsafe (unless those fields are all // `PhantomData`). true </ins> } <del> }); // We can't completely trust repr(C) markings; make sure the fields are // actually safe. </del> <ins> } else { false }; // We can't completely trust `repr(C)` markings, so make sure the fields are actually safe. </ins> let mut all_phantom = !variant.fields.is_empty(); for field in &variant.fields { <del> match self.check_field_type_for_ffi(cache, &field, args) { FfiSafe => { all_phantom = false; } FfiPhantom(..) if !def.repr().transparent() && def.is_enum() => { return FfiUnsafe { ty, reason: fluent::lint_improper_ctypes_enum_phantomdata, help: None, }; } FfiPhantom(..) => {} r => return transparent_safety.unwrap_or(r), </del> <ins> all_phantom &= match self.check_field_type_for_ffi(cache, &field, args) { FfiSafe => false, // `()` fields are FFI-safe! FfiUnsafe { ty, .. } if ty.is_unit() => false, FfiPhantom(..) => true, r @ FfiUnsafe { .. } => return r, </ins> } } <del> if all_phantom { FfiPhantom(ty) } else { transparent_safety.unwrap_or(FfiSafe) } </del> <ins> if all_phantom { FfiPhantom(ty) } else if transparent_with_all_zst_fields { FfiUnsafe { ty, reason: fluent::lint_improper_ctypes_struct_zst, help: None } } else { FfiSafe } </ins> } /// Checks if the given type is "ffi-safe" (has a stable, well-defined

} let sig = tcx.erase_late_bound_regions(sig); <del> if !sig.output().is_unit() { let r = self.check_type_for_ffi(cache, sig.output()); match r { FfiSafe => {} _ => { return r; } } } </del> for arg in sig.inputs() { <del> let r = self.check_type_for_ffi(cache, *arg); match r { </del> <ins> match self.check_type_for_ffi(cache, *arg) { </ins> FfiSafe => {} <del> _ => { return r; } </del> <ins> r => return r, </ins> } } <del> FfiSafe </del> <ins> let ret_ty = sig.output(); if ret_ty.is_unit() { return FfiSafe; } self.check_type_for_ffi(cache, ret_ty) </ins> } ty::Foreign(..) => FfiSafe,

} // Don't report FFI errors for unit return types. This check exists here, and not in <del> // `check_foreign_fn` (where it would make more sense) so that normalization has definitely </del> <ins> // the caller (where it would make more sense) so that normalization has definitely </ins> // happened. if is_return_type && ty.is_unit() { return;

None, ); } <del> // If `ty` is a `repr(transparent)` newtype, and the non-zero-sized type is a generic // argument, which after substitution, is `()`, then this branch can be hit. FfiResult::FfiUnsafe { ty, .. } if is_return_type && ty.is_unit() => {} </del> FfiResult::FfiUnsafe { ty, reason, help } => { self.emit_ffi_unsafe_type_lint(ty, sp, reason, help); }

<ins> #![deny(improper_ctypes_definitions)] #[repr(C)] pub struct Foo { a: u8, b: (), } extern "C" fn foo(x: Foo) -> Foo { todo!() } struct NotSafe(u32); #[repr(C)] pub struct Bar { a: u8, b: (), c: NotSafe, } extern "C" fn bar(x: Bar) -> Bar { //~^ ERROR `extern` fn uses type `NotSafe`, which is not FFI-safe //~^^ ERROR `extern` fn uses type `NotSafe`, which is not FFI-safe todo!() } fn main() {} </ins>

<ins> error: `extern` fn uses type `NotSafe`, which is not FFI-safe --> $DIR/lint-ctypes-113436-1.rs:22:22 | LL | extern "C" fn bar(x: Bar) -> Bar { |                      ^^^ not FFI-safe | = help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct = note: this struct has unspecified layout note: the type is defined here --> $DIR/lint-ctypes-113436-1.rs:13:1 | LL | struct NotSafe(u32); | ^^^^^^^^^^^^^^ note: the lint level is defined here --> $DIR/lint-ctypes-113436-1.rs:1:9 | LL | #![deny(improper_ctypes_definitions)] |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: `extern` fn uses type `NotSafe`, which is not FFI-safe --> $DIR/lint-ctypes-113436-1.rs:22:30 | LL | extern "C" fn bar(x: Bar) -> Bar { |                              ^^^ not FFI-safe | = help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct = note: this struct has unspecified layout note: the type is defined here --> $DIR/lint-ctypes-113436-1.rs:13:1 | LL | struct NotSafe(u32); | ^^^^^^^^^^^^^^ error: aborting due to 2 previous errors </ins>

<ins> // check-pass #![deny(improper_ctypes_definitions)] #[repr(C)] pub struct Wrap<T>(T); #[repr(transparent)] pub struct TransparentWrap<T>(T); pub extern "C" fn f() -> Wrap<()> { todo!() } const _: extern "C" fn() -> Wrap<()> = f; pub extern "C" fn ff() -> Wrap<Wrap<()>> { todo!() } const _: extern "C" fn() -> Wrap<Wrap<()>> = ff; pub extern "C" fn g() -> TransparentWrap<()> { todo!() } const _: extern "C" fn() -> TransparentWrap<()> = g; pub extern "C" fn gg() -> TransparentWrap<TransparentWrap<()>> { todo!() } const _: extern "C" fn() -> TransparentWrap<TransparentWrap<()>> = gg; fn main() {} </ins>