flense_derive/lib.rs
1//! Derive macros for [`flense`](https://docs.rs/flense).
2//!
3//! This crate provides [`macro@Adapter`], a derive that generates the `unsafe
4//! impl Adapter<...>` blocks `flense` needs, with the byte offset filled in
5//! correctly from [`offset_of!`](core::mem::offset_of).
6//!
7//! Use of this derive is never required; you could directly write these
8//! implementations yourself, but the derive lets you avoid explicitly writing
9//! the unsafe block and performs some safety checks.
10
11use proc_macro::TokenStream;
12use proc_macro2::TokenStream as TokenStream2;
13use quote::quote;
14use syn::{
15 Data,
16 DeriveInput,
17 Fields,
18 Path,
19 Token,
20 Type,
21 Visibility,
22 parse::{
23 Parse,
24 ParseStream,
25 },
26 parse_macro_input,
27 punctuated::Punctuated,
28 spanned::Spanned as _,
29 token,
30};
31
32/// Derives `flense`'s `Adapter` implementations for the annotated fields of a
33/// struct.
34///
35/// Annotate each field that should be lensable with `#[field(...)]`. Each entry
36/// is either:
37///
38/// * `Marker` - reference an existing `Field` implementation. The macro emits
39/// an `unsafe impl Adapter<Marker>` for the struct and a compile-time check
40/// that the field's type is exactly `<Marker as Field>::Type`.
41/// * `new Marker` - also mints a fresh marker type `Marker` (inheriting the
42/// struct's visibility) whose `Field::Type` is the field's declared type.
43/// * `new reflexive Marker` - as `new Marker`, and additionally emits an
44/// `Adapter<Marker>` impl for the field type itself (`<Marker as
45/// Field>::Type` at offset zero), so a bare value of that type can be lensed
46/// as `Marker`. You could write this `unsafe impl` yourself; letting the
47/// derive emit it avoids needing to author and maintain a `unsafe` block.
48///
49/// A single `#[field(...)]` may list several comma-separated entries, and the
50/// attribute may be repeated on one field.
51///
52/// ```
53/// use flense::prelude::*;
54/// # use flense_derive::Adapter;
55///
56/// enum Position {}
57/// impl Field for Position {
58/// type Type = [f32; 3];
59/// }
60///
61/// #[derive(Adapter)]
62/// struct Vertex {
63/// #[field(Position)]
64/// pos: [f32; 3],
65/// #[field(new Color)]
66/// color: [u8; 3],
67/// }
68///
69/// #[derive(Adapter)]
70/// struct Mesh {
71/// #[field(Position)]
72/// world_pos: [f32; 3],
73/// #[field(Color)]
74/// color: [u8; 3],
75/// }
76/// ```
77#[proc_macro_derive(Adapter, attributes(field))]
78pub fn derive_adapter(input: TokenStream) -> TokenStream {
79 let input = parse_macro_input!(input as DeriveInput);
80 expand(&input)
81 .unwrap_or_else(syn::Error::into_compile_error)
82 .into()
83}
84
85/// A single entry inside a `#[field(...)]` attribute: optional `new` and
86/// `reflexive` keywords followed by the marker path.
87struct FieldEntry {
88 is_new: bool,
89 is_reflexive: bool,
90 path: Path,
91}
92
93/// Consume a contextual keyword if it leads `input`.
94///
95/// These keywords (`new`, `reflexive`) are not Rust keywords, so treat a
96/// leading ident as the keyword only when it is followed by another path (so a
97/// marker literally named `new`, or a path like `new::Marker`, still parses as
98/// a plain reference). Returns whether the keyword was present and consumed.
99fn eat_keyword(input: ParseStream, keyword: &str) -> syn::Result<bool> {
100 let is_kw = input.peek(syn::Ident) && !input.peek2(Token![::]) && {
101 let fork = input.fork();
102 let ident: syn::Ident = fork.parse()?;
103 ident == keyword && !fork.is_empty() && !fork.peek(Token![,])
104 };
105 if is_kw {
106 let _: syn::Ident = input.parse()?;
107 }
108 Ok(is_kw)
109}
110
111impl Parse for FieldEntry {
112 fn parse(input: ParseStream) -> syn::Result<Self> {
113 let is_new = eat_keyword(input, "new")?;
114 let is_reflexive = eat_keyword(input, "reflexive")?;
115 let path: Path = input.parse()?;
116 Ok(Self {
117 is_new,
118 is_reflexive,
119 path,
120 })
121 }
122}
123
124fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
125 let Data::Struct(data) = &input.data else {
126 return Err(syn::Error::new(
127 input.span(),
128 "`Adapter` can only be derived for structs",
129 ));
130 };
131 let Fields::Named(fields) = &data.fields else {
132 return Err(syn::Error::new(
133 data.fields.span(),
134 "`Adapter` can only be derived for structs with named fields",
135 ));
136 };
137
138 // `#[repr(packed)]` fields may be under-aligned. Referencing an under-aligned
139 // field is unsound, so a sound `Adapter` is impossible. Reject it.
140 for attr in &input.attrs {
141 if !attr.path().is_ident("repr") {
142 continue;
143 }
144 let mut packed = false;
145 attr.parse_nested_meta(|meta| {
146 if meta.path.is_ident("packed") {
147 packed = true;
148 }
149 // Consume an optional `(N)` argument (e.g. `packed(2)`, `align(8)`)
150 // so the nested-meta parse does not error.
151 if meta.input.peek(token::Paren) {
152 let content;
153 syn::parenthesized!(content in meta.input);
154 let _: TokenStream2 = content.parse()?;
155 }
156 Ok(())
157 })?;
158 if packed {
159 return Err(syn::Error::new(
160 attr.span(),
161 "`Adapter` cannot be derived for `#[repr(packed)]` structs: \
162 packed fields may be under-aligned, which would make the \
163 generated `Adapter` unsound",
164 ));
165 }
166 }
167
168 let struct_name = &input.ident;
169 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
170
171 let mut out = TokenStream2::new();
172
173 for field in &fields.named {
174 let field_name = field.ident.as_ref().expect("named field has an ident");
175 let field_ty = &field.ty;
176
177 for attr in &field.attrs {
178 if !attr.path().is_ident("field") {
179 continue;
180 }
181 let entries =
182 attr.parse_args_with(Punctuated::<FieldEntry, Token![,]>::parse_terminated)?;
183 for entry in entries {
184 out.extend(emit_entry(
185 &entry,
186 &input.vis,
187 struct_name,
188 &impl_generics,
189 &ty_generics,
190 where_clause,
191 field_name,
192 field_ty,
193 )?);
194 }
195 }
196 }
197
198 Ok(out)
199}
200
201#[allow(clippy::too_many_arguments, reason = "splicing several syn fragments")]
202fn emit_entry(
203 entry: &FieldEntry,
204 vis: &Visibility,
205 struct_name: &syn::Ident,
206 impl_generics: &syn::ImplGenerics,
207 ty_generics: &syn::TypeGenerics,
208 where_clause: Option<&syn::WhereClause>,
209 field_name: &syn::Ident,
210 field_ty: &Type,
211) -> syn::Result<TokenStream2> {
212 let marker = &entry.path;
213
214 // For `new Marker`, define the marker type and its `Field` impl. The marker
215 // must be a single identifier, since it is declared as a new item.
216 let new_marker = if entry.is_new {
217 let Some(ident) = marker.get_ident() else {
218 return Err(syn::Error::new(
219 marker.span(),
220 "`new` markers must be a single identifier, not a path",
221 ));
222 };
223 quote! {
224 #vis enum #ident {}
225 #[automatically_derived]
226 impl ::flense::Field for #ident {
227 type Type = #field_ty;
228 }
229 }
230 } else {
231 TokenStream2::new()
232 };
233
234 // For a `reflexive` marker, also emit an `Adapter` impl for the field type
235 // itself, treating the whole value as the field at offset zero. This lets a
236 // bare `<Marker as Field>::Type` (e.g. `[f32; 3]`) be lensed as `Marker`
237 // directly, not just the struct it was declared in.
238 let reflexive_impl = if entry.is_reflexive {
239 quote! {
240 // SAFETY: `Self` *is* `<#marker as Field>::Type` (which is
241 // `#field_ty` for this `new` marker), so the field sits at offset
242 // zero and spans the whole value; the offset is trivially aligned
243 // because the value is already a valid, aligned instance of its own
244 // type.
245 //
246 // The self type is spelled as the concrete `#field_ty`, not the
247 // projection `<#marker as Field>::Type`: a projection self-type is
248 // not normalized by downstream coherence checks, so it would
249 // spuriously overlap any other `Adapter<#marker>` impl.
250 #[automatically_derived]
251 unsafe impl ::flense::Adapter<#marker> for #field_ty {
252 const OFFSET: usize = 0;
253 }
254 }
255 } else {
256 TokenStream2::new()
257 };
258
259 // For an existing marker, guard that the field's type is exactly `<Marker as
260 // Field>::Type`. `offset_of!` alone does not check this, and a mismatch would
261 // be unsound, effectively allowing a silent transmute. The check lives inside
262 // the `OFFSET` const so the struct's generics and `Self` are in scope. `T:
263 // SameType<U>` holds only when `T == U`. The `new` case needs no guard: its
264 // `Type` is defined to be the field type.
265 let type_guard = if entry.is_new {
266 TokenStream2::new()
267 } else {
268 quote! {
269 trait SameType<T> {}
270 impl<T> SameType<T> for T {}
271 fn assert_field_type<FLENSE_DERIVE_T: SameType<<#marker as ::flense::Field>::Type>>() {}
272 let _ = assert_field_type::<#field_ty>;
273 }
274 };
275
276 Ok(quote! {
277 #new_marker
278
279 #reflexive_impl
280
281 // SAFETY: `OFFSET` is produced by `offset_of!` on the annotated field,
282 // so it is the field's true byte offset; the field's type is checked to
283 // equal `<#marker as Field>::Type` (or defined to be it for `new`
284 // markers); and the offset is asserted to be aligned for that type, so
285 // the field pointer the lens derives is always well-aligned.
286 #[automatically_derived]
287 unsafe impl #impl_generics ::flense::Adapter<#marker>
288 for #struct_name #ty_generics #where_clause
289 {
290 const OFFSET: usize = {
291 #type_guard
292 let offset = ::core::mem::offset_of!(Self, #field_name);
293 // Second safeguard (independent of the `#[repr(packed)]`
294 // rejection): the field must sit at an offset aligned for its
295 // type, otherwise the references a lens hands out would be
296 // unaligned, hence unsound.
297 ::core::assert!(
298 offset % ::core::mem::align_of::<#field_ty>() == 0,
299 "flense: field is not aligned for its type; the derived `Adapter` would be unsound",
300 );
301 offset
302 };
303 }
304 })
305}