Somewhat interestingly, ()
, #[repr(C)] struct Zst;
, and #[repr(transparent)] struct Zst(());
all cause improper_ctypes
warnings, but #[repr(C)] struct Zst(());
doesn't.
Even more interestingly imo, the #[repr(transparent)]
warning is phrased
warning: `extern` block uses type `Zst`, which is not FFI-safe
--> src/lib.rs:2:16
|
2 | fn oops(_: Zst);
| ^^^ not FFI-safe
|
= note: this struct contains only zero-sized fields
note: the type is defined here
--> src/lib.rs:6:1
|
6 | struct Zst(());
| ^^^^^^^^^^
= note: `#[warn(improper_ctypes)]` on by default
which suggests that the lack of a warning for #[repr(C)]
is a bug, not intentional.
The improper_ctypes
warning can sometimes be a bit overeager (e.g. warning on references to improper_ctypes, despite the references themselves having a fully defined and guaranteed C ABI), but the warning does suggest that zero-sized structs do not have a stably guaranteed ABI through extern "C"
. (extern "Rust"
of course we are fully capable of changing whenever and however we want.)
If "target C" contains an extension that allows zero-sized structs to exist and passes them (and it's not just that it "permits" zero-sized type values by making them actually have size[1]), then we should probably still try to match "target C" though, so long as it doesn't make our semantics completely unreasonable.
Some people will say that if "target C" supports defining a struct with no fields and that creates a struct with a nonzero size, then
#[repr(C)]
should replicate that behavior in Rust on that target. I respectfully disagree in this case. It's certainly an extra pitfall involved in translating C headers to Rust, but there are already plenty of similar target specific pitfalls that should ideally be considered when translating less-portable C. (A notable one being that composing overalignment and underalignment in MSVC behaves differently than in Rust, due to how MSVC handles a split between "required" and "preferred" alignment.) ↩︎