xref: /linux/rust/zerocopy-derive/derive/into_bytes.rs (revision 12d5fc96ebbb3b948861805b32a4c3a96105ccd9)
1 // SPDX-License-Identifier: (BSD-2-Clause OR Apache-2.0) OR MIT
2 //
3 use proc_macro2::{Span, TokenStream};
4 use quote::quote;
5 use syn::{parse_quote, Data, DataEnum, DataStruct, DataUnion, Error, Ident, Type, WherePredicate};
6 
7 use crate::{
8     repr::{EnumRepr, StructUnionRepr},
9     util::{
10         generate_tag_enum, Ctx, DataExt, FieldBounds, ImplBlockBuilder, PaddingCheck, Trait,
11         TraitBound,
12     },
13 };
14 pub(crate) fn derive_into_bytes(ctx: &Ctx, _top_level: Trait) -> Result<TokenStream, Error> {
15     match &ctx.ast.data {
16         Data::Struct(strct) => derive_into_bytes_struct(ctx, strct),
17         Data::Enum(enm) => derive_into_bytes_enum(ctx, enm),
18         Data::Union(unn) => derive_into_bytes_union(ctx, unn),
19     }
20 }
21 
22 /// If every field is exactly `T`, `[T; _]`, or a final `[T]` for the same type
23 /// parameter `T`, returns the bounds required to prove this to rustc.
24 fn homogeneous_field_bounds(ctx: &Ctx, strct: &DataStruct) -> Option<Vec<WherePredicate>> {
25     fn strip_parens_and_groups(mut ty: &Type) -> &Type {
26         loop {
27             ty = match ty {
28                 Type::Group(group) => &group.elem,
29                 Type::Paren(paren) => &paren.elem,
30                 ty => return ty,
31             };
32         }
33     }
34 
35     fn type_is_parameter(ty: &Type, parameter: &Ident) -> bool {
36         match strip_parens_and_groups(ty) {
37             Type::Path(path) => path.qself.is_none() && path.path.is_ident(parameter),
38             _ => false,
39         }
40     }
41 
42     let fields = strct.fields();
43     ctx.ast.generics.type_params().find_map(|parameter| {
44         let parameter = &parameter.ident;
45         let elements = fields
46             .iter()
47             .enumerate()
48             .map(|(index, (_, _, ty))| match strip_parens_and_groups(ty) {
49                 Type::Array(array) => Some(&*array.elem),
50                 Type::Slice(slice) if index + 1 == fields.len() => Some(&*slice.elem),
51                 Type::Slice(_) => None,
52                 ty => Some(ty),
53             })
54             .collect::<Option<Vec<_>>>()?;
55 
56         if !elements.iter().all(|element| type_is_parameter(element, parameter)) {
57             return None;
58         }
59 
60         let zerocopy_crate = &ctx.zerocopy_crate;
61         let mut bounds = fields
62             .iter()
63             .map(|(_, _, ty)| parse_quote!(#ty: #zerocopy_crate::IntoBytes))
64             .collect::<Vec<WherePredicate>>();
65 
66         // `Ident` equality is only a syntactic prefilter above. In
67         // macro-generated input, two identifiers can both print as `T` and
68         // compare equal here while having different hygiene contexts. For
69         // example, a call-site `T` could resolve to a `u8` type alias while a
70         // def-site `T` resolves to this struct's type parameter. Instantiating
71         // that parameter with `u16` would make the fields actually be `u8` and
72         // `u16`; treating them as homogeneous could overlook padding and cause
73         // us to emit an unsound `IntoBytes` impl.
74         //
75         // Quote every element type from its original field AST so that its
76         // hygiene context is retained, then use `Identity` to have rustc verify
77         // that it resolves to `parameter`. Do not deduplicate these predicates:
78         // identically-printed types may have different hygiene contexts and
79         // must each be checked by rustc.
80         bounds.extend(elements.into_iter().map(|element| {
81             parse_quote! {
82                 #element: #zerocopy_crate::util::macro_util::Identity<Type = #parameter>
83             }
84         }));
85         Some(bounds)
86     })
87 }
88 
89 fn derive_into_bytes_struct(ctx: &Ctx, strct: &DataStruct) -> Result<TokenStream, Error> {
90     let repr = StructUnionRepr::from_attrs(&ctx.ast.attrs)?;
91 
92     let is_transparent = repr.is_transparent();
93     let is_c = repr.is_c();
94     let is_packed_1 = repr.is_packed_1();
95     let num_fields = strct.fields().len();
96     let mut homogeneous_bounds =
97         if is_c && !repr.is_align_gt_1() { homogeneous_field_bounds(ctx, strct) } else { None };
98 
99     let (padding_check, require_unaligned_fields, explicit_field_bounds) = if is_transparent
100         || is_packed_1
101     {
102         // No padding check needed.
103         // - repr(transparent): The layout and ABI of the whole struct is the
104         //   same as its only non-ZST field (meaning there's no padding outside
105         //   of that field) and we require that field to be `IntoBytes` (meaning
106         //   there's no padding in that field).
107         // - repr(packed): Any inter-field padding bytes are removed, meaning
108         //   that any padding bytes would need to come from the fields, all of
109         //   which we require to be `IntoBytes` (meaning they don't have any
110         //   padding). Note that this holds regardless of other `repr`
111         //   attributes, including `repr(Rust)`. [1]
112         //
113         // [1] Per https://doc.rust-lang.org/1.81.0/reference/type-layout.html#the-alignment-modifiers:
114         //
115         //   An important consequence of these rules is that a type with
116         //   `#[repr(packed(1))]`` (or `#[repr(packed)]``) will have no
117         //   inter-field padding.
118         (None, false, None)
119     } else if is_c && !repr.is_align_gt_1() && num_fields <= 1 {
120         // No padding check needed. A repr(C) struct with zero or one field has
121         // no padding unless #[repr(align)] explicitly adds padding, which we
122         // check for in this branch's condition.
123         (None, false, None)
124     } else if ctx.ast.generics.params.is_empty() {
125         // Is the last field a syntactic slice, i.e., `[SomeType]`.
126         let is_syntactic_dst =
127             strct.fields().last().map(|(_, _, ty)| matches!(ty, Type::Slice(_))).unwrap_or(false);
128         // Since there are no generics, we can emit a padding check. All reprs
129         // guarantee that fields won't overlap [1], so the padding check is
130         // sound. This is more permissive than the next case, which requires
131         // that all field types implement `Unaligned`.
132         //
133         // [1] Per https://doc.rust-lang.org/1.81.0/reference/type-layout.html#the-rust-representation:
134         //
135         //   The only data layout guarantees made by [`repr(Rust)`] are those
136         //   required for soundness. They are:
137         //   ...
138         //   2. The fields do not overlap.
139         //   ...
140         if is_c && is_syntactic_dst {
141             (Some(PaddingCheck::ReprCStruct), false, None)
142         } else {
143             (Some(PaddingCheck::Struct), false, None)
144         }
145     } else if let Some(bounds) = homogeneous_bounds.take() {
146         // Let `a` be the alignment of `T` and `s` be its size. Rust guarantees
147         // that `s` is a multiple of `a`. `T`, `[T; N]`, and `[T]` all have
148         // alignment `a`, and their sizes are `s`, `N * s`, and `len * s`,
149         // respectively.
150         //
151         // Without a `packed` modifier, each field and the struct have
152         // alignment `a`. `align(1)` does not change that because every Rust
153         // type already has alignment at least 1. With `packed(P)`, each field
154         // and the struct instead have alignment `b = min(a, P)`. Rust
155         // alignments and valid values of `P` are powers of two, so `b` divides
156         // `a`; every field size is therefore also a multiple of `b`.
157         //
158         // Thus, after placing any field, the running offset is already aligned
159         // for the next field, and the final offset is already aligned for the
160         // struct. A repr(C) without an `align` modifier greater than 1
161         // therefore introduces neither inter-field nor trailing padding,
162         // including for a slice DST. The bounds returned by
163         // `homogeneous_field_bounds` require rustc to verify that the element
164         // type of every field is the same type `T`.
165         (None, false, Some(bounds))
166     } else if is_c && !repr.is_align_gt_1() {
167         // We can't use a padding check since there are generic type arguments.
168         // Instead, we require all field types to implement `Unaligned`. This
169         // ensures that the `repr(C)` layout algorithm will not insert any
170         // padding unless #[repr(align)] explicitly adds padding, which we check
171         // for in this branch's condition.
172         //
173         // FIXME(#10): Support type parameters for non-transparent, non-packed
174         // structs without requiring `Unaligned`.
175         (None, true, None)
176     } else {
177         return ctx.error_or_skip(Error::new(
178             Span::call_site(),
179             "must have a non-align #[repr(...)] attribute in order to guarantee this type's memory layout",
180         ));
181     };
182 
183     let field_bounds = if let Some(bounds) = explicit_field_bounds {
184         FieldBounds::Explicit(bounds)
185     } else if require_unaligned_fields {
186         FieldBounds::All(&[TraitBound::Slf, TraitBound::Other(Trait::Unaligned)])
187     } else {
188         FieldBounds::ALL_SELF
189     };
190 
191     Ok(ImplBlockBuilder::new(ctx, strct, Trait::IntoBytes, field_bounds)
192         .padding_check(padding_check)
193         .build())
194 }
195 
196 fn derive_into_bytes_enum(ctx: &Ctx, enm: &DataEnum) -> Result<TokenStream, Error> {
197     let repr = EnumRepr::from_attrs(&ctx.ast.attrs)?;
198     if !repr.is_c() && !repr.is_primitive() {
199         return ctx.error_or_skip(Error::new(
200             Span::call_site(),
201             "must have #[repr(C)] or #[repr(Int)] attribute in order to guarantee this type's memory layout",
202         ));
203     }
204 
205     let tag_type_definition = generate_tag_enum(ctx, &repr, enm);
206     Ok(ImplBlockBuilder::new(ctx, enm, Trait::IntoBytes, FieldBounds::ALL_SELF)
207         .padding_check(PaddingCheck::Enum { tag_type_definition })
208         .build())
209 }
210 
211 fn derive_into_bytes_union(ctx: &Ctx, unn: &DataUnion) -> Result<TokenStream, Error> {
212     // See #1792 for more context.
213     //
214     // By checking for `zerocopy_derive_union_into_bytes` both here and in the
215     // generated code, we ensure that `--cfg zerocopy_derive_union_into_bytes`
216     // need only be passed *either* when compiling this crate *or* when
217     // compiling the user's crate. The former is preferable, but in some
218     // situations (such as when cross-compiling using `cargo build --target`),
219     // it doesn't get propagated to this crate's build by default.
220     let cfg_compile_error = if cfg!(zerocopy_derive_union_into_bytes) {
221         quote!()
222     } else {
223         let core = ctx.core_path();
224         let error_message = "requires --cfg zerocopy_derive_union_into_bytes;
225 please let us know you use this feature: https://github.com/google/zerocopy/discussions/1802";
226         quote!(
227             #[allow(unused_attributes, unexpected_cfgs)]
228             const _: () = {
229                 #[cfg(not(zerocopy_derive_union_into_bytes))]
230                 #core::compile_error!(#error_message);
231             };
232         )
233     };
234 
235     // FIXME(#10): Support type parameters.
236     if !ctx.ast.generics.params.is_empty() {
237         return ctx.error_or_skip(Error::new(
238             Span::call_site(),
239             "unsupported on types with type parameters",
240         ));
241     }
242 
243     // Because we don't support generics, we don't need to worry about
244     // special-casing different reprs. So long as there is *some* repr which
245     // guarantees the layout, our `PaddingCheck::Union` guarantees that there is
246     // no padding.
247     let repr = StructUnionRepr::from_attrs(&ctx.ast.attrs)?;
248     if !repr.is_c() && !repr.is_transparent() && !repr.is_packed_1() {
249         return ctx.error_or_skip(Error::new(
250             Span::call_site(),
251             "must be #[repr(C)], #[repr(packed)], or #[repr(transparent)]",
252         ));
253     }
254 
255     let impl_block = ImplBlockBuilder::new(ctx, unn, Trait::IntoBytes, FieldBounds::ALL_SELF)
256         .padding_check(PaddingCheck::Union)
257         .build();
258     Ok(quote!(#cfg_compile_error #impl_block))
259 }
260 
261 #[cfg(test)]
262 mod tests {
263     use super::*;
264 
265     fn derive(input: syn::DeriveInput) -> TokenStream {
266         let ctx = Ctx::try_from_derive_input(input).unwrap();
267         derive_into_bytes(&ctx, Trait::IntoBytes).unwrap()
268     }
269 
270     #[test]
271     fn raw_repr_matches_ordinary_repr() {
272         let ordinary = derive(parse_quote! {
273             #[repr(C)]
274             #[repr(align(8))]
275             struct Response {
276                 status: u8,
277             }
278         });
279         let raw = derive(parse_quote! {
280             #[repr(C)]
281             #[r#repr(r#align(8))]
282             struct Response {
283                 status: u8,
284             }
285         });
286 
287         let ordinary = ordinary.to_string();
288         let raw = raw.to_string();
289 
290         // In particular, both expansions contain the padding check which
291         // prevents the resulting type from implementing `IntoBytes`.
292         assert!(ordinary.contains("PaddingFree"));
293         assert_eq!(ordinary, raw);
294     }
295 }
296