photon_ring_derive/lib.rs
1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Derive macros for [`photon_ring::Pod`] and [`photon_ring::Message`].
5//!
6//! ## `Pod` derive
7//!
8//! ```ignore
9//! #[derive(photon_ring::Pod)]
10//! struct Quote {
11//! price: f64,
12//! volume: u32,
13//! }
14//! ```
15//!
16//! This generates compile-time assertions that every field implements `Pod`,
17//! plus `unsafe impl photon_ring::Pod for Quote {}`.
18//!
19//! **Note:** The macro does *not* add `#[repr(C)]` or `Clone`/`Copy` derives.
20//! You must add those yourself for the `Pod` contract to hold.
21//!
22//! ## `Message` derive
23//!
24//! ```ignore
25//! #[derive(photon_ring::Message)]
26//! struct Order {
27//! price: f64,
28//! qty: u32,
29//! #[photon(as_enum)]
30//! side: Side, // any #[repr(u8)] enum — requires #[photon(as_enum)]
31//! filled: bool,
32//! tag: Option<u32>,
33//! }
34//! ```
35//!
36//! Generates a Pod-compatible wire struct (`OrderWire`) plus `From`
37//! conversions in both directions. See [`derive_message`] for details.
38
39use proc_macro::TokenStream;
40use proc_macro2::Span;
41use quote::{format_ident, quote};
42use syn::{
43 parse_macro_input, Data, DeriveInput, Fields, GenericArgument, Meta, PathArguments, Type,
44};
45
46/// Derive `Pod` for a struct.
47///
48/// Requirements:
49/// - Must be a struct (not enum or union).
50/// - All fields must implement `Pod`.
51/// - The user must add `#[repr(C)]`, `Clone`, and `Copy` themselves;
52/// the macro only emits field assertions and `unsafe impl Pod`.
53///
54/// # Example
55///
56/// ```ignore
57/// #[derive(photon_ring::Pod)]
58/// struct Tick {
59/// price: f64,
60/// volume: u32,
61/// _pad: u32,
62/// }
63/// ```
64#[proc_macro_derive(Pod)]
65pub fn derive_pod(input: TokenStream) -> TokenStream {
66 let input = parse_macro_input!(input as DeriveInput);
67 let name = &input.ident;
68 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
69
70 // Verify #[repr(C)] is present
71 let has_repr_c = input.attrs.iter().any(|attr| {
72 if !attr.path().is_ident("repr") {
73 return false;
74 }
75 let mut found = false;
76 if let Meta::List(list) = &attr.meta {
77 let _ = list.parse_nested_meta(|nested| {
78 if nested.path.is_ident("C") {
79 found = true;
80 }
81 Ok(())
82 });
83 }
84 found
85 });
86 if !has_repr_c {
87 return syn::Error::new_spanned(
88 &input.ident,
89 "Pod can only be derived for #[repr(C)] structs",
90 )
91 .to_compile_error()
92 .into();
93 }
94
95 // Only structs are supported
96 let fields = match &input.data {
97 Data::Struct(s) => match &s.fields {
98 Fields::Named(f) => f.named.iter().collect::<Vec<_>>(),
99 Fields::Unnamed(f) => f.unnamed.iter().collect::<Vec<_>>(),
100 Fields::Unit => vec![],
101 },
102 _ => {
103 return syn::Error::new_spanned(&input.ident, "Pod can only be derived for structs")
104 .to_compile_error()
105 .into();
106 }
107 };
108
109 // Generate compile-time assertions that every field is Pod
110 let field_assertions = fields.iter().map(|f| {
111 let ty = &f.ty;
112 quote! {
113 const _: () = {
114 fn _assert_pod<T: photon_ring::Pod>() {}
115 fn _check() { _assert_pod::<#ty>(); }
116 };
117 }
118 });
119
120 let expanded = quote! {
121 // Compile-time field checks
122 #(#field_assertions)*
123
124 // Safety: all fields verified to be Pod via compile-time assertions above.
125 // The derive macro only applies to structs, and Pod requires that every
126 // bit pattern is valid — which holds when all fields are Pod.
127 unsafe impl #impl_generics photon_ring::Pod for #name #ty_generics #where_clause {}
128 };
129
130 TokenStream::from(expanded)
131}
132
133// ---------------------------------------------------------------------------
134// Message derive
135// ---------------------------------------------------------------------------
136
137/// Classification of a field type for wire conversion.
138enum FieldKind {
139 /// Numeric or array — passes through unchanged.
140 Passthrough,
141 /// `bool` → `u8`.
142 Bool,
143 /// `usize` → `u64`.
144 Usize,
145 /// `isize` → `i64`.
146 Isize,
147 /// `Option<T>` where T is an unsigned ≤64-bit integer type (u8, u16, u32, u64).
148 /// Wire struct gets two fields: `X_value: u64` and `X_has: u8`.
149 /// Stores the inner type for the back-conversion cast.
150 OptionUnsignedInt(Type),
151 /// `Option<T>` where T is a signed ≤64-bit integer type (i8, i16, i32, i64).
152 /// Wire struct gets two fields: `X_value: i64` and `X_has: u8`.
153 /// Stores the inner type for the back-conversion cast.
154 OptionSignedInt(Type),
155 /// `Option<bool>` — wire struct gets `X_value: u8` and `X_has: u8`.
156 OptionBool,
157 /// `Option<u128>` — wire struct gets `X_value: u128` and `X_has: u8`.
158 OptionU128,
159 /// `Option<i128>` — wire struct gets `X_value: u128` and `X_has: u8`.
160 OptionI128,
161 /// `Option<usize>` — wire struct gets `X_value: u64` and `X_has: u8`.
162 OptionUsize,
163 /// `Option<isize>` — wire struct gets `X_value: i64` and `X_has: u8`.
164 OptionIsize,
165 /// `Option<f32>` — wire struct gets `X_value: u32` (bit-encoded) and `X_has: u8`.
166 OptionF32,
167 /// `Option<f64>` — wire struct gets `X_value: u64` (bit-encoded) and `X_has: u8`.
168 OptionF64,
169 /// A `#[repr(u8)]` enum, explicitly marked with `#[photon(as_enum)]` → `u8`.
170 Enum,
171 /// Unrecognized type — will emit a compile error.
172 Unsupported,
173 /// Unsupported `Option<T>` inner type — will emit a compile error.
174 UnsupportedOption(String),
175}
176
177/// Returns the type name string for a simple path type, or `None`.
178fn type_name(ty: &Type) -> Option<String> {
179 if let Type::Path(p) = ty {
180 if let Some(seg) = p.path.segments.last() {
181 return Some(seg.ident.to_string());
182 }
183 }
184 None
185}
186
187/// Classify a field's type into a [`FieldKind`].
188fn classify(ty: &Type) -> FieldKind {
189 match ty {
190 // Arrays `[T; N]` — passthrough (must be Pod).
191 Type::Array(_) => FieldKind::Passthrough,
192
193 Type::Path(p) => {
194 let seg = match p.path.segments.last() {
195 Some(s) => s,
196 None => return FieldKind::Unsupported,
197 };
198 let id = seg.ident.to_string();
199
200 match id.as_str() {
201 // Numerics — passthrough
202 "u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128"
203 | "f32" | "f64" => FieldKind::Passthrough,
204
205 "bool" => FieldKind::Bool,
206 "usize" => FieldKind::Usize,
207 "isize" => FieldKind::Isize,
208
209 "Option" => {
210 // Extract inner type from Option<T>
211 if let PathArguments::AngleBracketed(args) = &seg.arguments {
212 if let Some(GenericArgument::Type(inner)) = args.args.first() {
213 let name = type_name(inner).unwrap_or_default();
214 return match name.as_str() {
215 "bool" => FieldKind::OptionBool,
216 "f32" => FieldKind::OptionF32,
217 "f64" => FieldKind::OptionF64,
218 "u128" => FieldKind::OptionU128,
219 "i128" => FieldKind::OptionI128,
220 "usize" => FieldKind::OptionUsize,
221 "isize" => FieldKind::OptionIsize,
222 "u8" | "u16" | "u32" | "u64" => {
223 FieldKind::OptionUnsignedInt(inner.clone())
224 }
225 "i8" | "i16" | "i32" | "i64" => {
226 FieldKind::OptionSignedInt(inner.clone())
227 }
228 _ => FieldKind::UnsupportedOption(name),
229 };
230 }
231 }
232 FieldKind::UnsupportedOption(String::new())
233 }
234
235 // Anything else — unrecognized, require explicit attribute
236 _ => FieldKind::Unsupported,
237 }
238 }
239
240 _ => FieldKind::Unsupported,
241 }
242}
243
244/// Derive a Pod-compatible wire struct with `From` conversions.
245///
246/// Given a struct with fields that may include `bool`, `Option<numeric>`,
247/// `usize`/`isize`, and `#[repr(u8)]` enums, generates:
248///
249/// 1. **`{Name}Wire`** — a `#[repr(C)] Clone + Copy` struct with all fields
250/// converted to Pod-safe types, plus `unsafe impl Pod`.
251/// 2. **`From<Name> for {Name}Wire`** — converts the domain struct to wire.
252/// 3. **`{Name}Wire::into_domain(self) -> Name`** — converts the wire struct
253/// back. This is an `unsafe` method for structs containing enum fields
254/// (since the enum discriminant is not validated), or a safe `From` impl
255/// for structs without enum fields.
256///
257/// # Field type mappings
258///
259/// | Source type | Wire type | To wire | From wire |
260/// |---|---|---|---|
261/// | `f32`, `f64`, `u8`..`u128`, `i8`..`i128` | same | passthrough | passthrough |
262/// | `usize` | `u64` | `as u64` | `as usize` |
263/// | `isize` | `i64` | `as i64` | `as isize` |
264/// | `bool` | `u8` | `if v { 1 } else { 0 }` | `v != 0` |
265/// | `Option<T>` (T: unsigned ≤64-bit) | `X_value: u64, X_has: u8` | `Some(v) => (v as u64, 1), None => (0, 0)` | `has != 0 => Some(value as T), else None` |
266/// | `Option<T>` (T: signed ≤64-bit) | `X_value: i64, X_has: u8` | `Some(v) => (v as i64, 1), None => (0, 0)` | `has != 0 => Some(value as T), else None` |
267/// | `Option<u128>` | `X_value: u128, X_has: u8` | `Some(v) => (v, 1), None => (0, 0)` | `has != 0 => Some(value), else None` |
268/// | `Option<i128>` | `X_value: u128, X_has: u8` | `Some(v) => (v as u128, 1), None => (0, 0)` | `has != 0 => Some(value as i128), else None` |
269/// | `Option<usize>` | `X_value: u64, X_has: u8` | `Some(v) => (v as u64, 1), None => (0, 0)` | `has != 0 => Some(value as usize), else None` |
270/// | `Option<isize>` | `X_value: i64, X_has: u8` | `Some(v) => (v as i64, 1), None => (0, 0)` | `has != 0 => Some(value as isize), else None` |
271/// | `Option<f32>` | `X_value: u32, X_has: u8` | `Some(v) => (v.to_bits(), 1), None => (0, 0)` | `has != 0 => Some(f32::from_bits(value)), else None` |
272/// | `Option<f64>` | `X_value: u64, X_has: u8` | `Some(v) => (v.to_bits(), 1), None => (0, 0)` | `has != 0 => Some(f64::from_bits(value)), else None` |
273/// | `[T; N]` (T: Pod) | same | passthrough | passthrough |
274/// | `#[photon(as_enum)] field: E` | `u8` | `v as u8` | `transmute(v)` (unsafe) |
275///
276/// # Enum fields
277///
278/// Enum fields **must** be annotated with `#[photon(as_enum)]` to opt in
279/// to the `u8` wire encoding. Without this attribute, unrecognized types
280/// produce a compile error. The enum must have `#[repr(u8)]` — the macro
281/// emits a compile-time `size_of` check to enforce this.
282///
283/// Enum fields are stored as raw `u8` on the wire. Converting back requires
284/// that the byte holds a valid discriminant. Because the macro cannot verify
285/// enum variants at compile time, structs with enum fields generate an
286/// `unsafe fn into_domain(self) -> DomainType` method on the wire struct
287/// instead of a safe `From` impl. Callers must ensure enum fields contain
288/// valid discriminants (which is always the case when the wire data was
289/// produced by a valid domain value via `From<Domain> for Wire`).
290///
291/// # Example
292///
293/// ```ignore
294/// #[repr(u8)]
295/// #[derive(Clone, Copy)]
296/// enum Side { Buy = 0, Sell = 1 }
297///
298/// #[derive(photon_ring::Message)]
299/// struct Order {
300/// price: f64,
301/// qty: u32,
302/// #[photon(as_enum)]
303/// side: Side,
304/// filled: bool,
305/// tag: Option<u32>,
306/// }
307/// // Generates: OrderWire, From<Order> for OrderWire,
308/// // OrderWire::into_domain (unsafe, due to enum field)
309/// ```
310#[proc_macro_derive(Message, attributes(photon))]
311pub fn derive_message(input: TokenStream) -> TokenStream {
312 let input = parse_macro_input!(input as DeriveInput);
313 let name = &input.ident;
314 let wire_name = format_ident!("{}Wire", name);
315
316 // Only named structs are supported
317 let fields = match &input.data {
318 Data::Struct(s) => match &s.fields {
319 Fields::Named(f) => f.named.iter().collect::<Vec<_>>(),
320 _ => {
321 return syn::Error::new_spanned(
322 &input.ident,
323 "Message can only be derived for structs with named fields",
324 )
325 .to_compile_error()
326 .into();
327 }
328 },
329 _ => {
330 return syn::Error::new_spanned(
331 &input.ident,
332 "Message can only be derived for structs",
333 )
334 .to_compile_error()
335 .into();
336 }
337 };
338
339 let mut wire_fields = Vec::new();
340 let mut to_wire = Vec::new();
341 let mut from_wire = Vec::new();
342 let mut assertions = Vec::new();
343 let mut has_enum_fields = false;
344 let mut has_usize_isize = false;
345
346 for field in &fields {
347 let fname = field.ident.as_ref().unwrap();
348 let fty = &field.ty;
349
350 // Check for #[photon(as_enum)] attribute
351 let is_explicit_enum = field.attrs.iter().any(|attr| {
352 if attr.path().is_ident("photon") {
353 if let Ok(meta) = attr.parse_args::<syn::Ident>() {
354 return meta == "as_enum";
355 }
356 }
357 false
358 });
359
360 let kind = if is_explicit_enum {
361 FieldKind::Enum
362 } else {
363 classify(fty)
364 };
365
366 match kind {
367 FieldKind::Passthrough => {
368 wire_fields.push(quote! { pub #fname: #fty });
369 to_wire.push(quote! { #fname: src.#fname });
370 from_wire.push(quote! { #fname: src.#fname });
371 }
372 FieldKind::Bool => {
373 wire_fields.push(quote! { pub #fname: u8 });
374 to_wire.push(quote! { #fname: if src.#fname { 1 } else { 0 } });
375 from_wire.push(quote! { #fname: src.#fname != 0 });
376 }
377 FieldKind::Usize => {
378 has_usize_isize = true;
379 wire_fields.push(quote! { pub #fname: u64 });
380 to_wire.push(quote! { #fname: src.#fname as u64 });
381 from_wire.push(quote! { #fname: src.#fname as usize });
382 }
383 FieldKind::Isize => {
384 has_usize_isize = true;
385 wire_fields.push(quote! { pub #fname: i64 });
386 to_wire.push(quote! { #fname: src.#fname as i64 });
387 from_wire.push(quote! { #fname: src.#fname as isize });
388 }
389 FieldKind::OptionUnsignedInt(inner) => {
390 let value_field = format_ident!("{}_value", fname);
391 let has_field = format_ident!("{}_has", fname);
392 wire_fields.push(quote! { pub #value_field: u64 });
393 wire_fields.push(quote! { pub #has_field: u8 });
394 to_wire.push(quote! {
395 #value_field: match src.#fname {
396 Some(v) => v as u64,
397 None => 0,
398 }
399 });
400 to_wire.push(quote! {
401 #has_field: if src.#fname.is_some() { 1 } else { 0 }
402 });
403 from_wire.push(quote! {
404 #fname: if src.#has_field != 0 {
405 Some(src.#value_field as #inner)
406 } else {
407 None
408 }
409 });
410 }
411 FieldKind::OptionSignedInt(inner) => {
412 let value_field = format_ident!("{}_value", fname);
413 let has_field = format_ident!("{}_has", fname);
414 wire_fields.push(quote! { pub #value_field: i64 });
415 wire_fields.push(quote! { pub #has_field: u8 });
416 to_wire.push(quote! {
417 #value_field: match src.#fname {
418 Some(v) => v as i64,
419 None => 0,
420 }
421 });
422 to_wire.push(quote! {
423 #has_field: if src.#fname.is_some() { 1 } else { 0 }
424 });
425 from_wire.push(quote! {
426 #fname: if src.#has_field != 0 {
427 Some(src.#value_field as #inner)
428 } else {
429 None
430 }
431 });
432 }
433 FieldKind::OptionBool => {
434 let value_field = format_ident!("{}_value", fname);
435 let has_field = format_ident!("{}_has", fname);
436 wire_fields.push(quote! { pub #value_field: u8 });
437 wire_fields.push(quote! { pub #has_field: u8 });
438 to_wire.push(quote! {
439 #value_field: match src.#fname {
440 Some(v) => if v { 1 } else { 0 },
441 None => 0,
442 }
443 });
444 to_wire.push(quote! {
445 #has_field: if src.#fname.is_some() { 1 } else { 0 }
446 });
447 from_wire.push(quote! {
448 #fname: if src.#has_field != 0 {
449 Some(src.#value_field != 0)
450 } else {
451 None
452 }
453 });
454 }
455 FieldKind::OptionU128 => {
456 let value_field = format_ident!("{}_value", fname);
457 let has_field = format_ident!("{}_has", fname);
458 wire_fields.push(quote! { pub #value_field: u128 });
459 wire_fields.push(quote! { pub #has_field: u8 });
460 to_wire.push(quote! {
461 #value_field: match src.#fname {
462 Some(v) => v,
463 None => 0,
464 }
465 });
466 to_wire.push(quote! {
467 #has_field: if src.#fname.is_some() { 1 } else { 0 }
468 });
469 from_wire.push(quote! {
470 #fname: if src.#has_field != 0 {
471 Some(src.#value_field)
472 } else {
473 None
474 }
475 });
476 }
477 FieldKind::OptionI128 => {
478 let value_field = format_ident!("{}_value", fname);
479 let has_field = format_ident!("{}_has", fname);
480 wire_fields.push(quote! { pub #value_field: u128 });
481 wire_fields.push(quote! { pub #has_field: u8 });
482 to_wire.push(quote! {
483 #value_field: match src.#fname {
484 Some(v) => v as u128,
485 None => 0,
486 }
487 });
488 to_wire.push(quote! {
489 #has_field: if src.#fname.is_some() { 1 } else { 0 }
490 });
491 from_wire.push(quote! {
492 #fname: if src.#has_field != 0 {
493 Some(src.#value_field as i128)
494 } else {
495 None
496 }
497 });
498 }
499 FieldKind::OptionUsize => {
500 has_usize_isize = true;
501 let value_field = format_ident!("{}_value", fname);
502 let has_field = format_ident!("{}_has", fname);
503 wire_fields.push(quote! { pub #value_field: u64 });
504 wire_fields.push(quote! { pub #has_field: u8 });
505 to_wire.push(quote! {
506 #value_field: match src.#fname {
507 Some(v) => v as u64,
508 None => 0,
509 }
510 });
511 to_wire.push(quote! {
512 #has_field: if src.#fname.is_some() { 1 } else { 0 }
513 });
514 from_wire.push(quote! {
515 #fname: if src.#has_field != 0 {
516 Some(src.#value_field as usize)
517 } else {
518 None
519 }
520 });
521 }
522 FieldKind::OptionIsize => {
523 has_usize_isize = true;
524 let value_field = format_ident!("{}_value", fname);
525 let has_field = format_ident!("{}_has", fname);
526 wire_fields.push(quote! { pub #value_field: i64 });
527 wire_fields.push(quote! { pub #has_field: u8 });
528 to_wire.push(quote! {
529 #value_field: match src.#fname {
530 Some(v) => v as i64,
531 None => 0,
532 }
533 });
534 to_wire.push(quote! {
535 #has_field: if src.#fname.is_some() { 1 } else { 0 }
536 });
537 from_wire.push(quote! {
538 #fname: if src.#has_field != 0 {
539 Some(src.#value_field as isize)
540 } else {
541 None
542 }
543 });
544 }
545 FieldKind::OptionF32 => {
546 let value_field = format_ident!("{}_value", fname);
547 let has_field = format_ident!("{}_has", fname);
548 wire_fields.push(quote! { pub #value_field: u32 });
549 wire_fields.push(quote! { pub #has_field: u8 });
550 to_wire.push(quote! {
551 #value_field: match src.#fname {
552 Some(v) => v.to_bits(),
553 None => 0,
554 }
555 });
556 to_wire.push(quote! {
557 #has_field: if src.#fname.is_some() { 1 } else { 0 }
558 });
559 from_wire.push(quote! {
560 #fname: if src.#has_field != 0 {
561 Some(f32::from_bits(src.#value_field))
562 } else {
563 None
564 }
565 });
566 }
567 FieldKind::OptionF64 => {
568 let value_field = format_ident!("{}_value", fname);
569 let has_field = format_ident!("{}_has", fname);
570 wire_fields.push(quote! { pub #value_field: u64 });
571 wire_fields.push(quote! { pub #has_field: u8 });
572 to_wire.push(quote! {
573 #value_field: match src.#fname {
574 Some(v) => v.to_bits(),
575 None => 0,
576 }
577 });
578 to_wire.push(quote! {
579 #has_field: if src.#fname.is_some() { 1 } else { 0 }
580 });
581 from_wire.push(quote! {
582 #fname: if src.#has_field != 0 {
583 Some(f64::from_bits(src.#value_field))
584 } else {
585 None
586 }
587 });
588 }
589 FieldKind::Enum => {
590 has_enum_fields = true;
591 wire_fields.push(quote! { pub #fname: u8 });
592 to_wire.push(quote! { #fname: src.#fname as u8 });
593 from_wire.push(quote! {
594 // SAFETY: This transmute converts a raw u8 back to the enum type.
595 // This is sound ONLY when the byte contains a valid discriminant.
596 // The wire struct should only be constructed via `From<DomainType>`,
597 // which guarantees valid discriminants. Constructing the wire struct
598 // from arbitrary bytes and calling `into_domain()` is undefined
599 // behavior if any enum field holds an invalid discriminant.
600 #fname: unsafe { core::mem::transmute::<u8, #fty>(src.#fname) }
601 });
602 // Compile-time assertion: enum must be 1 byte (#[repr(u8)])
603 let msg = format!(
604 "Message derive: field `{}` has type `{}` which is not 1 byte. \
605 Enum fields must have #[repr(u8)].",
606 fname,
607 quote! { #fty },
608 );
609 let msg_lit = syn::LitStr::new(&msg, Span::call_site());
610 assertions.push(quote! {
611 const _: () = {
612 assert!(
613 core::mem::size_of::<#fty>() == 1,
614 #msg_lit,
615 );
616 };
617 });
618 }
619 FieldKind::Unsupported => {
620 let msg = format!(
621 "Unsupported field type `{}`. Use #[photon(as_enum)] for #[repr(u8)] enum fields, \
622 or convert to a numeric type manually.",
623 quote!(#fty),
624 );
625 return syn::Error::new_spanned(fty, msg).to_compile_error().into();
626 }
627 FieldKind::UnsupportedOption(inner_name) => {
628 let msg = format!(
629 "Message derive: field `{}` has unsupported type `Option<{}>`. \
630 Only Option<bool>, Option<integer>, Option<f32>, and Option<f64> \
631 are supported.",
632 fname, inner_name,
633 );
634 return syn::Error::new_spanned(fty, msg).to_compile_error().into();
635 }
636 }
637 }
638
639 // H5: Compile-time assertion that usize/isize fit in u64/i64 (documents
640 // the 64-bit assumption and fails loudly on platforms where it does not hold).
641 if has_usize_isize {
642 assertions.push(quote! {
643 const _: () = assert!(
644 core::mem::size_of::<usize>() <= core::mem::size_of::<u64>(),
645 "photon-ring Message derive requires usize to fit in u64",
646 );
647 });
648 }
649
650 // If the struct has enum fields, generate an unsafe `into_domain` method
651 // instead of a safe `From` impl to avoid exposing transmute through safe code.
652 let from_wire_impl = if has_enum_fields {
653 quote! {
654 impl #wire_name {
655 /// Convert wire struct back to domain struct.
656 ///
657 /// # Safety
658 ///
659 /// Enum fields are stored as raw `u8` and converted back via
660 /// `core::mem::transmute`. The caller **must** ensure every enum
661 /// field contains a valid discriminant value. This is guaranteed
662 /// when the wire struct was produced by `From<DomainType>` — but
663 /// constructing the wire struct from arbitrary bytes (e.g. reading
664 /// raw memory, deserialization) and calling this method is
665 /// **undefined behavior** if any enum field holds an invalid
666 /// discriminant.
667 #[inline]
668 pub unsafe fn into_domain(self) -> #name {
669 let src = self;
670 #name {
671 #(#from_wire),*
672 }
673 }
674 }
675 }
676 } else {
677 quote! {
678 impl From<#wire_name> for #name {
679 #[inline]
680 fn from(src: #wire_name) -> Self {
681 #name {
682 #(#from_wire),*
683 }
684 }
685 }
686 }
687 };
688
689 // C3: Add a doc warning on the wire struct when it contains enum fields
690 let wire_struct_doc = if has_enum_fields {
691 quote! {
692 /// Auto-generated Pod-compatible wire struct for the domain type.
693 ///
694 /// # Warning
695 ///
696 /// This struct contains enum fields stored as raw `u8`. Constructing
697 /// it from arbitrary bytes (not via `From<DomainType>`) and then calling
698 /// `into_domain()` can cause **undefined behavior** if any enum field
699 /// holds an invalid discriminant value.
700 }
701 } else {
702 quote! {
703 /// Auto-generated Pod-compatible wire struct for the domain type.
704 }
705 };
706
707 let expanded = quote! {
708 // Compile-time assertions
709 #(#assertions)*
710
711 #wire_struct_doc
712 #[repr(C)]
713 #[derive(Clone, Copy)]
714 pub struct #wire_name {
715 #(#wire_fields),*
716 }
717
718 // Safety: all fields of the wire struct are plain numeric types
719 // (u8, u32, u64, f32, f64, etc.) where every bit pattern is valid.
720 unsafe impl photon_ring::Pod for #wire_name {}
721
722 impl From<#name> for #wire_name {
723 #[inline]
724 fn from(src: #name) -> Self {
725 #wire_name {
726 #(#to_wire),*
727 }
728 }
729 }
730
731 #from_wire_impl
732 };
733
734 TokenStream::from(expanded)
735}