diff --git a/zerocopy-derive/src/derive/into_bytes.rs b/zerocopy-derive/src/derive/into_bytes.rs index 8c1e1009dd..3c5c06fda2 100644 --- a/zerocopy-derive/src/derive/into_bytes.rs +++ b/zerocopy-derive/src/derive/into_bytes.rs @@ -1,5 +1,5 @@ use proc_macro2::{Span, TokenStream}; -use quote::quote; +use quote::{quote, ToTokens}; use syn::{Data, DataEnum, DataStruct, DataUnion, Error, Type}; use crate::{ @@ -68,6 +68,23 @@ fn derive_into_bytes_struct(ctx: &Ctx, strct: &DataStruct) -> Result()`, field `i` sits at offset `i * + // size_of::()` (always a multiple of `align_of::()`), and + // the struct's total size is `N * size_of::()` (also a + // multiple of its alignment). Hence there is no inter-field or + // trailing padding, and requiring `T: IntoBytes` (so that `T` + // itself is padding-free) proves the struct is padding-free. + // + // We prefer this to the `Unaligned`-requiring branch below + // because it accepts strictly more types without giving up any + // soundness guarantees. + (None, false) } else if is_c && !repr.is_align_gt_1() { // We can't use a padding check since there are generic type arguments. // Instead, we require all field types to implement `Unaligned`. This @@ -96,6 +113,16 @@ fn derive_into_bytes_struct(ctx: &Ctx, strct: &DataStruct) -> Result bool { + let fields = strct.fields(); + let mut fields = fields.into_iter().map(|(_, _, ty)| ty.into_token_stream().to_string()); + if let Some(first) = fields.next() { + fields.all(|field| field == first) + } else { + true + } +} + fn derive_into_bytes_enum(ctx: &Ctx, enm: &DataEnum) -> Result { let repr = EnumRepr::from_attrs(&ctx.ast.attrs)?; if !repr.is_c() && !repr.is_primitive() { diff --git a/zerocopy-derive/src/output_tests/expected/into_bytes_struct_homogeneous_generic.expected.rs b/zerocopy-derive/src/output_tests/expected/into_bytes_struct_homogeneous_generic.expected.rs new file mode 100644 index 0000000000..c9dbf0453a --- /dev/null +++ b/zerocopy-derive/src/output_tests/expected/into_bytes_struct_homogeneous_generic.expected.rs @@ -0,0 +1,21 @@ +#[allow( + deprecated, + private_bounds, + non_local_definitions, + non_camel_case_types, + non_upper_case_globals, + non_snake_case, + non_ascii_idents, + clippy::missing_inline_in_public_items, +)] +#[deny(ambiguous_associated_items)] +#[automatically_derived] +const _: () = { + unsafe impl ::zerocopy::IntoBytes for Foo + where + T: ::zerocopy::IntoBytes, + T: ::zerocopy::IntoBytes, + { + fn only_derive_is_allowed_to_implement_this_trait() {} + } +}; diff --git a/zerocopy-derive/src/output_tests/expected/into_bytes_struct_homogeneous_generic_assoc_ty.expected.rs b/zerocopy-derive/src/output_tests/expected/into_bytes_struct_homogeneous_generic_assoc_ty.expected.rs new file mode 100644 index 0000000000..e787acfb9c --- /dev/null +++ b/zerocopy-derive/src/output_tests/expected/into_bytes_struct_homogeneous_generic_assoc_ty.expected.rs @@ -0,0 +1,22 @@ +#[allow( + deprecated, + private_bounds, + non_local_definitions, + non_camel_case_types, + non_upper_case_globals, + non_snake_case, + non_ascii_idents, + clippy::missing_inline_in_public_items, +)] +#[deny(ambiguous_associated_items)] +#[automatically_derived] +const _: () = { + unsafe impl ::zerocopy::IntoBytes for Foo

+ where + P::BaseField: ::zerocopy::IntoBytes, + P::BaseField: ::zerocopy::IntoBytes, + P::BaseField: ::zerocopy::IntoBytes, + { + fn only_derive_is_allowed_to_implement_this_trait() {} + } +}; diff --git a/zerocopy-derive/src/output_tests/mod.rs b/zerocopy-derive/src/output_tests/mod.rs index 836eb6c0a8..a75c1da759 100644 --- a/zerocopy-derive/src/output_tests/mod.rs +++ b/zerocopy-derive/src/output_tests/mod.rs @@ -232,6 +232,41 @@ fn test_into_bytes_struct_trailing() { } } +#[test] +fn test_into_bytes_struct_homogeneous_generic() { + // A `#[repr(C)]` generic struct whose fields all share the same + // syntactic type has no padding, so the emitted `IntoBytes` impl + // bounds only on `IntoBytes` for the shared type and omits the + // `Unaligned` bound that the fallback branch would otherwise add. + test! { + IntoBytes { + #[repr(C)] + struct Foo { + a: T, + b: T, + } + } expands to "expected/into_bytes_struct_homogeneous_generic.expected.rs" + } +} + +#[test] +fn test_into_bytes_struct_homogeneous_generic_assoc_ty() { + // Exercises the case in which every field is an associated-type + // projection of the single type parameter. Token comparison sees + // the same sequence `P :: BaseField` for every field, so the + // homogeneous branch applies. + test! { + IntoBytes { + #[repr(C)] + struct Foo { + c0: P::BaseField, + c1: P::BaseField, + c2: P::BaseField, + } + } expands to "expected/into_bytes_struct_homogeneous_generic_assoc_ty.expected.rs" + } +} + #[test] fn test_into_bytes_struct_trailing_generic() { test! { diff --git a/zerocopy-derive/tests/struct_to_bytes.rs b/zerocopy-derive/tests/struct_to_bytes.rs index 40fa2e5a91..f5dd2de9bf 100644 --- a/zerocopy-derive/tests/struct_to_bytes.rs +++ b/zerocopy-derive/tests/struct_to_bytes.rs @@ -160,6 +160,33 @@ struct ReprCGenericOneField { util_assert_impl_all!(ReprCGenericOneField: imp::IntoBytes); util_assert_impl_all!(ReprCGenericOneField<[util::AU16]>: imp::IntoBytes); +// When every field of a generic `repr(C)` struct has the same syntactic +// type, the struct has no padding regardless of the alignment of that +// type, so `IntoBytes` can be derived requiring only that the shared +// type itself is `IntoBytes` (no `Unaligned` bound needed). + +#[derive(imp::IntoBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct ReprCGenericHomogeneousTuple(T, T); + +util_assert_impl_all!(ReprCGenericHomogeneousTuple: imp::IntoBytes); +util_assert_impl_all!(ReprCGenericHomogeneousTuple: imp::IntoBytes); +util_assert_impl_all!(ReprCGenericHomogeneousTuple: imp::IntoBytes); + +#[derive(imp::IntoBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct ReprCGenericHomogeneousNamed { + x: T, + y: T, + z: T, +} + +util_assert_impl_all!(ReprCGenericHomogeneousNamed: imp::IntoBytes); +util_assert_impl_all!(ReprCGenericHomogeneousNamed: imp::IntoBytes); +util_assert_impl_all!(ReprCGenericHomogeneousNamed: imp::IntoBytes); + #[derive(imp::IntoBytes)] #[zerocopy(crate = "zerocopy_renamed")] #[repr(C)]