zerocopy_derive/derive/into_bytes.rs
1// SPDX-License-Identifier: (BSD-2-Clause OR Apache-2.0) OR MIT
2
3use proc_macro2::{Span, TokenStream};
4use quote::quote;
5use syn::{Data, DataEnum, DataStruct, DataUnion, Error, Type};
6
7use crate::{
8 repr::{EnumRepr, StructUnionRepr},
9 util::{
10 generate_tag_enum, Ctx, DataExt, FieldBounds, ImplBlockBuilder, PaddingCheck, Trait,
11 TraitBound,
12 },
13};
14pub(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}
21fn derive_into_bytes_struct(ctx: &Ctx, strct: &DataStruct) -> Result<TokenStream, Error> {
22 let repr = StructUnionRepr::from_attrs(&ctx.ast.attrs)?;
23
24 let is_transparent = repr.is_transparent();
25 let is_c = repr.is_c();
26 let is_packed_1 = repr.is_packed_1();
27 let num_fields = strct.fields().len();
28
29 let (padding_check, require_unaligned_fields) = if is_transparent || is_packed_1 {
30 // No padding check needed.
31 // - repr(transparent): The layout and ABI of the whole struct is the
32 // same as its only non-ZST field (meaning there's no padding outside
33 // of that field) and we require that field to be `IntoBytes` (meaning
34 // there's no padding in that field).
35 // - repr(packed): Any inter-field padding bytes are removed, meaning
36 // that any padding bytes would need to come from the fields, all of
37 // which we require to be `IntoBytes` (meaning they don't have any
38 // padding). Note that this holds regardless of other `repr`
39 // attributes, including `repr(Rust)`. [1]
40 //
41 // [1] Per https://doc.rust-lang.org/1.81.0/reference/type-layout.html#the-alignment-modifiers:
42 //
43 // An important consequence of these rules is that a type with
44 // `#[repr(packed(1))]`` (or `#[repr(packed)]``) will have no
45 // inter-field padding.
46 (None, false)
47 } else if is_c && !repr.is_align_gt_1() && num_fields <= 1 {
48 // No padding check needed. A repr(C) struct with zero or one field has
49 // no padding unless #[repr(align)] explicitly adds padding, which we
50 // check for in this branch's condition.
51 (None, false)
52 } else if ctx.ast.generics.params.is_empty() {
53 // Is the last field a syntactic slice, i.e., `[SomeType]`.
54 let is_syntactic_dst =
55 strct.fields().last().map(|(_, _, ty)| matches!(ty, Type::Slice(_))).unwrap_or(false);
56 // Since there are no generics, we can emit a padding check. All reprs
57 // guarantee that fields won't overlap [1], so the padding check is
58 // sound. This is more permissive than the next case, which requires
59 // that all field types implement `Unaligned`.
60 //
61 // [1] Per https://doc.rust-lang.org/1.81.0/reference/type-layout.html#the-rust-representation:
62 //
63 // The only data layout guarantees made by [`repr(Rust)`] are those
64 // required for soundness. They are:
65 // ...
66 // 2. The fields do not overlap.
67 // ...
68 if is_c && is_syntactic_dst {
69 (Some(PaddingCheck::ReprCStruct), false)
70 } else {
71 (Some(PaddingCheck::Struct), false)
72 }
73 } else if is_c && !repr.is_align_gt_1() {
74 // We can't use a padding check since there are generic type arguments.
75 // Instead, we require all field types to implement `Unaligned`. This
76 // ensures that the `repr(C)` layout algorithm will not insert any
77 // padding unless #[repr(align)] explicitly adds padding, which we check
78 // for in this branch's condition.
79 //
80 // FIXME(#10): Support type parameters for non-transparent, non-packed
81 // structs without requiring `Unaligned`.
82 (None, true)
83 } else {
84 return ctx.error_or_skip(Error::new(
85 Span::call_site(),
86 "must have a non-align #[repr(...)] attribute in order to guarantee this type's memory layout",
87 ));
88 };
89
90 let field_bounds = if require_unaligned_fields {
91 FieldBounds::All(&[TraitBound::Slf, TraitBound::Other(Trait::Unaligned)])
92 } else {
93 FieldBounds::ALL_SELF
94 };
95
96 Ok(ImplBlockBuilder::new(ctx, strct, Trait::IntoBytes, field_bounds)
97 .padding_check(padding_check)
98 .build())
99}
100
101fn derive_into_bytes_enum(ctx: &Ctx, enm: &DataEnum) -> Result<TokenStream, Error> {
102 let repr = EnumRepr::from_attrs(&ctx.ast.attrs)?;
103 if !repr.is_c() && !repr.is_primitive() {
104 return ctx.error_or_skip(Error::new(
105 Span::call_site(),
106 "must have #[repr(C)] or #[repr(Int)] attribute in order to guarantee this type's memory layout",
107 ));
108 }
109
110 let tag_type_definition = generate_tag_enum(ctx, &repr, enm);
111 Ok(ImplBlockBuilder::new(ctx, enm, Trait::IntoBytes, FieldBounds::ALL_SELF)
112 .padding_check(PaddingCheck::Enum { tag_type_definition })
113 .build())
114}
115
116fn derive_into_bytes_union(ctx: &Ctx, unn: &DataUnion) -> Result<TokenStream, Error> {
117 // See #1792 for more context.
118 //
119 // By checking for `zerocopy_derive_union_into_bytes` both here and in the
120 // generated code, we ensure that `--cfg zerocopy_derive_union_into_bytes`
121 // need only be passed *either* when compiling this crate *or* when
122 // compiling the user's crate. The former is preferable, but in some
123 // situations (such as when cross-compiling using `cargo build --target`),
124 // it doesn't get propagated to this crate's build by default.
125 let cfg_compile_error = if cfg!(zerocopy_derive_union_into_bytes) {
126 quote!()
127 } else {
128 let core = ctx.core_path();
129 let error_message = "requires --cfg zerocopy_derive_union_into_bytes;
130please let us know you use this feature: https://github.com/google/zerocopy/discussions/1802";
131 quote!(
132 #[allow(unused_attributes, unexpected_cfgs)]
133 const _: () = {
134 #[cfg(not(zerocopy_derive_union_into_bytes))]
135 #core::compile_error!(#error_message);
136 };
137 )
138 };
139
140 // FIXME(#10): Support type parameters.
141 if !ctx.ast.generics.params.is_empty() {
142 return ctx.error_or_skip(Error::new(
143 Span::call_site(),
144 "unsupported on types with type parameters",
145 ));
146 }
147
148 // Because we don't support generics, we don't need to worry about
149 // special-casing different reprs. So long as there is *some* repr which
150 // guarantees the layout, our `PaddingCheck::Union` guarantees that there is
151 // no padding.
152 let repr = StructUnionRepr::from_attrs(&ctx.ast.attrs)?;
153 if !repr.is_c() && !repr.is_transparent() && !repr.is_packed_1() {
154 return ctx.error_or_skip(Error::new(
155 Span::call_site(),
156 "must be #[repr(C)], #[repr(packed)], or #[repr(transparent)]",
157 ));
158 }
159
160 let impl_block = ImplBlockBuilder::new(ctx, unn, Trait::IntoBytes, FieldBounds::ALL_SELF)
161 .padding_check(PaddingCheck::Union)
162 .build();
163 Ok(quote!(#cfg_compile_error #impl_block))
164}