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 = ¶meter.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