Skip to main content

zerodds_flatdata_derive/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Crate `zerodds-flatdata-derive`. Safety classification: **STANDARD**
5//! (proc-macro generiert `unsafe impl FlatStruct` — Layout-Garantien
6//! werden vom Macro selbst geprueft, nicht vom Caller-Kommentar).
7//!
8//! `#[derive(FlatStruct)]` fuer
9//! [`zerodds_flatdata::FlatStruct`](https://docs.rs/zerodds-flatdata).
10//!
11//! Spec: `docs/specs/zerodds-flatdata-1.0.md` §1.2 (Derive-Macro).
12//!
13//! ## Schichten-Position
14//!
15//! Layer 4 — Core Services (proc-macro fuer `zerodds-flatdata`).
16//!
17//! ## Public API (Stand 1.0.0-rc.1)
18//!
19//! - `#[derive(FlatStruct)]` — generiert `unsafe impl FlatStruct for T`
20//!   mit `TYPE_HASH = sha256(type_name + field_layout)[..16]`.
21//!
22//! ## Compile-Time-Checks
23//!
24//! Der Macro lehnt mit `compile_error!` ab, wenn:
25//! - `T` ist `enum` oder `union` (Layout nicht stable).
26//! - `T` traegt weder `#[repr(C)]` noch `#[repr(transparent)]`
27//!   (Default-Repr ist undefiniert).
28//!
29//! Die `Copy + 'static + Send + Sync`-Bounds werden vom Trait selbst
30//! erzwungen — der Compiler emittiert verstaendliche Fehler beim
31//! Versuch, ein Non-`Copy`-Type zu deriven.
32//!
33//! ## Beispiel
34//!
35//! ```ignore
36//! use zerodds_flatdata_derive::FlatStruct;
37//!
38//! #[derive(Copy, Clone, FlatStruct)]
39//! #[repr(C)]
40//! struct Pose { x: f64, y: f64, z: f64 }
41//! ```
42
43#![warn(missing_docs)]
44
45use proc_macro::TokenStream;
46use proc_macro2::TokenStream as TokenStream2;
47use quote::quote;
48use sha2::{Digest, Sha256};
49use syn::{Attribute, Data, DeriveInput, Fields, parse_macro_input};
50
51/// `#[derive(FlatStruct)]` — generiert `unsafe impl FlatStruct for T`.
52#[proc_macro_derive(FlatStruct)]
53pub fn derive_flat_struct(input: TokenStream) -> TokenStream {
54    let input = parse_macro_input!(input as DeriveInput);
55    expand(input).unwrap_or_else(|e| e.to_compile_error().into())
56}
57
58fn expand(input: DeriveInput) -> Result<TokenStream, syn::Error> {
59    let name = &input.ident;
60    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
61
62    let layout_string = match &input.data {
63        Data::Struct(s) => layout_signature(name, &s.fields),
64        Data::Enum(_) => {
65            return Err(syn::Error::new_spanned(
66                &input,
67                "FlatStruct kann nicht auf enum derive werden — repr(C)-Enum-Layout ist nicht stable",
68            ));
69        }
70        Data::Union(_) => {
71            return Err(syn::Error::new_spanned(
72                &input,
73                "FlatStruct kann nicht auf union derive werden",
74            ));
75        }
76    };
77
78    if !has_repr_c_or_transparent(&input.attrs) {
79        return Err(syn::Error::new_spanned(
80            &input,
81            "FlatStruct verlangt #[repr(C)] oder #[repr(transparent)] — \
82             Default-repr-Rust hat undefiniertes Field-Layout, daher \
83             waere `as_bytes()`/`from_bytes_unchecked()` UB",
84        ));
85    }
86
87    let mut hasher = Sha256::new();
88    hasher.update(layout_string.as_bytes());
89    let digest = hasher.finalize();
90    let hash_bytes: [u8; 16] = match digest[..16].try_into() {
91        Ok(b) => b,
92        Err(_) => {
93            return Err(syn::Error::new_spanned(
94                &input,
95                "internal: sha256 truncate to 16 bytes failed",
96            ));
97        }
98    };
99    let hash_tokens = hash_bytes.iter().map(|b| quote! { #b });
100
101    let expanded: TokenStream2 = quote! {
102        // SAFETY: derive(FlatStruct) prueft `repr(C)`/`repr(transparent)`
103        // und lehnt enum/union ab. Trait-Bounds `Copy + 'static + Send +
104        // Sync` werden vom Compiler ueber die Trait-Definition erzwungen.
105        // TYPE_HASH ist SHA-256 ueber `<TypeName>{<field>:<ty>,...}`,
106        // erkennt Type-Rename / Field-add/remove / Field-reorder /
107        // Field-Type-Change.
108        #[automatically_derived]
109        unsafe impl #impl_generics ::zerodds_flatdata::FlatStruct for #name #ty_generics #where_clause {
110            const TYPE_HASH: [u8; 16] = [#( #hash_tokens ),*];
111        }
112    };
113    Ok(expanded.into())
114}
115
116fn has_repr_c_or_transparent(attrs: &[Attribute]) -> bool {
117    for attr in attrs {
118        if !attr.path().is_ident("repr") {
119            continue;
120        }
121        let mut found = false;
122        let _ = attr.parse_nested_meta(|meta| {
123            if meta.path.is_ident("C") || meta.path.is_ident("transparent") {
124                found = true;
125            }
126            Ok(())
127        });
128        if found {
129            return true;
130        }
131    }
132    false
133}
134
135/// Erzeugt einen Layout-String fuer SHA-256.
136///
137/// Format: `<TypeName>{<field-name>:<field-ty-string>,...}`. Damit
138/// erkennt der Hash:
139/// - Type-Rename → neuer Hash.
140/// - Field-add/remove → neuer Hash.
141/// - Field-reorder → neuer Hash.
142/// - Field-Type-Change → neuer Hash.
143fn layout_signature(name: &syn::Ident, fields: &Fields) -> String {
144    let mut s = name.to_string();
145    s.push('{');
146    match fields {
147        Fields::Named(f) => {
148            for (i, field) in f.named.iter().enumerate() {
149                if i > 0 {
150                    s.push(',');
151                }
152                if let Some(id) = &field.ident {
153                    s.push_str(&id.to_string());
154                    s.push(':');
155                }
156                s.push_str(&type_signature(&field.ty));
157            }
158        }
159        Fields::Unnamed(f) => {
160            for (i, field) in f.unnamed.iter().enumerate() {
161                if i > 0 {
162                    s.push(',');
163                }
164                s.push_str(&type_signature(&field.ty));
165            }
166        }
167        Fields::Unit => {
168            s.push_str("()");
169        }
170    }
171    s.push('}');
172    s
173}
174
175fn type_signature(ty: &syn::Type) -> String {
176    quote! { #ty }.to_string().replace(' ', "")
177}