xdr_derive/lib.rs
1//! Derive macros for XDR serialization and type representation.
2//!
3//! This crate provides procedural macros that automatically generate
4//! implementations of `ReadXDR`, `WriteXDR`, and `XDR` traits for your Rust
5//! types, allowing them to be serialized/deserialized to/from [XDR][RFC-4506]
6//! format and to describe their XDR type representation.
7//!
8//! ## Macros
9//!
10//! - **`#[derive(ReadXDR)]`**: Automatically implements the `ReadXDR` trait
11//! for structs and enums, enabling deserialization from XDR-encoded data.
12//!
13//! - **`#[derive(WriteXDR)]`**: Automatically implements the `WriteXDR` trait
14//! for structs and enums, enabling serialization to XDR-encoded data.
15//!
16//! - **`#[derive(XDR)]`**: Automatically implements the `XDR` and `HasTypeDef`
17//! traits, allowing types to describe their XDR type representation for
18//! generation of specifications.
19//!
20//! ## Examples
21//!
22//! ### Serialization and Deserialization
23//!
24//! ```rust
25//! use xdrs::{ReadXDR, WriteXDR, cxs};
26//!
27//! #[derive(ReadXDR, WriteXDR)]
28//! struct Person {
29//! name: Vec<u8>,
30//! age: u32,
31//! }
32//!
33//! let person = Person {
34//! name: b"Alice".to_vec(),
35//! age: 30,
36//! };
37//!
38//! // Serialize
39//! let encoded = cxs::encode(&person).unwrap();
40//!
41//! // Deserialize
42//! let decoded: Person = cxs::decode(&encoded).unwrap();
43//!
44//! assert_eq!(person.name, decoded.name);
45//! assert_eq!(person.age, decoded.age);
46//! ```
47//!
48//! ### XDR Type Representation
49//!
50//! ```rust
51//! use xdrs::xdr_repr::XDR;
52//!
53//! #[derive(xdrs::XDR)]
54//! struct Message {
55//! id: u32,
56//! data: [u8; 16],
57//! }
58//!
59//! // Generate XDR specification
60//! let xdr_def = Message::def();
61//!
62//! // The generated XDR specification would be:
63//! //
64//! // struct Message {
65//! // unsigned int id;
66//! // opaque data[16];
67//! // };
68//! # let _ = xdr_def;
69//! ```
70//!
71//! ### Enums
72//!
73//! ```rust
74//! use xdrs::{ReadXDR, WriteXDR, cxs};
75//!
76//! #[derive(ReadXDR, WriteXDR, PartialEq, Debug)]
77//! #[repr(i32)]
78//! enum Status {
79//! Success = 0,
80//! Error { msg: Vec<u8> } = 1,
81//! }
82//!
83//! let status = Status::Success;
84//!
85//! let encoded = cxs::encode(&status).unwrap();
86//! let decoded: Status = cxs::decode(&encoded).unwrap();
87//!
88//! assert_eq!(status, decoded);
89//! ```
90//!
91//! ## Attributes
92//!
93//! The derive macros support various attributes for customization. When
94//! deriving `XDR`:
95//!
96//! - **`#[xdr(name = "XdrName")]`**: Use a different name in the XDR
97//! specification
98//! - **`#[xdr(discrim_type = "TypeName")]`**: Specify a custom discriminant
99//! type for enums
100//! - **`#[xdr(discrim_var = "field_name")]`**: Specify a custom discriminant
101//! field name
102//!
103//! When deriving `ReadXDR`:
104//!
105//! - `#[xdr(from_type = "Type")]` - Deserialize to the intermediate type
106//! "Type" before converting into the target type via `Into`.
107//! - `#[xdr(via = "Type")]` - Same as above.
108//!
109//! ### ReadXDR Notes
110//!
111//! The lifetime name `'_r` is reserved for the generated reader lifetime. Types
112//! deriving `ReadXDR` must not declare their own generic lifetime with that
113//! name.
114//!
115//! When deriving `WriteXDR`:
116//!
117//! - `#[xdr(into_type = "Type")]` - Convert (via `Into`) a different type before
118//! serializing.
119//! - `#[xdr(via = "Type")]` - Same as above.
120//!
121//! When deriving `ReadXDR`, `WriteXDR`, or `XDR` on an enum, the trailing
122//! variant may be marked `#[xdr(default_variant)]` to act as the union's
123//! default arm; see the individual derive macros for details.
124//!
125//! ### Crate path override
126//!
127//! All three derives accept `#[xdr(crate = "path")]` to override the default
128//! `::xdrs` import path (useful when the dependency is renamed or re-exported).
129//!
130//! [RFC-4506]: https://datatracker.ietf.org/doc/html/rfc4506
131
132#![warn(missing_docs)]
133
134mod attrs;
135mod generate;
136mod generate_const;
137mod generate_type;
138mod util;
139
140use proc_macro::TokenStream;
141
142/// Derives the `WriteXDR` trait for a struct or enum.
143///
144/// This macro generates an implementation that serializes the type to XDR format.
145/// All field types must implement `WriteXDR`. It also implements
146/// `EncodesAs<Self>`, so the type can be passed where an `EncodesAs` bound
147/// expects it.
148///
149/// # Attributes
150///
151/// - `#[xdr(into_type = "Type")]` - Convert (via `Into`) a different type before
152/// serializing.
153/// - `#[xdr(via = "Type")]` - Same as above (also implies `from_type`).
154/// - `#[xdr(default_variant)]` - On an enum variant: mark it as the union's
155/// default arm. The variant must be the trailing variant and must not
156/// specify a discriminant, and its first field must be `i32`, which stores
157/// the wire discriminant and is emitted verbatim when encoding.
158/// - `#[xdr(crate = "path::to::xdrs")]` - Override the path to the `xdrs` crate
159/// (useful when re-exporting from another crate).
160///
161/// # Limitations
162///
163/// - **No recursive types.** XDR encoding is not self-describing; a recursive
164/// type will loop at runtime. The macro does not detect or reject recursion.
165/// In particular, `#[xdr(into_type = "Self")]` is an infinite loop — the
166/// generated impl converts `self` into `Self` (a no-op) and re-enters the
167/// same encoder until the stack overflows. The surrogate type must always be
168/// a **distinct**, non-recursive type.
169/// - **All field types must be serializable.** Every field (or the surrogate
170/// type, when using `into_type`/`via`) must independently satisfy `WriteXDR`.
171/// The macro copies the input type's generic bounds verbatim — it does **not**
172/// infer or propagate trait bounds required by a surrogate type's own generic
173/// parameters. For example, `#[xdr(into_type = "VecM<T, 8>")]` on a generic
174/// `Wrapper<T>` will not automatically add `T: WriteXDR`; you must supply the
175/// bound yourself.
176/// - **Default variant discriminant is not validated.** When a `Default(i32, ..)`
177/// variant is encoded, the writer emits the stored discriminant without
178/// checking whether it collides with a concrete variant. Encoding a `Default`
179/// with a known discriminant value will produce bytes that the reader decodes
180/// as the concrete variant, not as `Default`.
181///
182#[proc_macro_derive(WriteXDR, attributes(xdr, xdrs))]
183pub fn derive_write_xdr(input: TokenStream) -> TokenStream {
184 let input = syn::parse_macro_input!(input as syn::DeriveInput);
185
186 generate::write_xdr(&input)
187 .unwrap_or_else(syn::Error::into_compile_error)
188 .into()
189}
190
191/// Derives the `ReadXDR` trait for a struct or enum.
192///
193/// This macro generates an implementation that deserializes the type from XDR
194/// format. All field types must implement `ReadXDR`.
195///
196/// # Attributes
197///
198/// - `#[xdr(from_type = "Type")]` - Deserialize to the intermediate type
199/// "Type" before converting into the target type via `Into`.
200/// - `#[xdr(via = "Type")]` - Same as above (also implies `into_type`).
201/// - `#[xdr(default_variant)]` - On an enum variant (which must be the
202/// trailing variant): mark it as the union's default arm. Discriminants that
203/// match no other variant decode to this variant, with the wire discriminant
204/// stored in its first field (which must be `i32`). Without a default
205/// variant, unknown discriminants fail with a decode error.
206/// - `#[xdr(crate = "path::to::xdrs")]` - Override the path to the `xdrs` crate
207/// (useful when re-exporting from another crate).
208///
209/// # Limitations
210///
211/// - **No recursive types.** XDR decoding is not self-describing; a recursive
212/// type will loop at runtime. The macro does not detect or reject recursion.
213/// In particular, `#[xdr(from_type = "Self")]` is an infinite loop — the
214/// generated impl reads a `Self` and converts it into `Self` (a no-op),
215/// re-entering the same decoder until the stack overflows. The surrogate type
216/// must always be a **distinct**, non-recursive type.
217/// - **All field types must be deserializable.** Every field (or the surrogate
218/// type, when using `from_type`/`via`) must independently satisfy `ReadXDR`.
219/// The macro copies the input type's generic bounds verbatim — it does **not**
220/// infer or propagate trait bounds required by a surrogate type's own generic
221/// parameters. For example, `#[xdr(from_type = "VecM<T, 8>")]` on a generic
222/// `Wrapper<T>` will not automatically add `T: ReadXDR`; you must supply the
223/// bound yourself.
224/// - **Borrowed fields need a general lifetime implementation.** A custom
225/// field type `Borrowed<'a>` must implement `ReadXDR<'r>` whenever `'r: 'a`,
226/// just like the built-in borrowed types. Implementing only
227/// `ReadXDR<'a> for Borrowed<'a>` does not suffice for in-place decoding:
228/// the field's type is fixed behind a mutable pointer, so the compiler
229/// cannot shorten the returned value's lifetime at the assignment site.
230/// Use `impl<'r: 'a, 'a> ReadXDR<'r> for Borrowed<'a>`.
231/// - **The lifetime name `'_r` is reserved.** The generated `ReadXDR` impl uses
232/// `'_r` as its reader lifetime. A type deriving `ReadXDR` must not declare a
233/// generic lifetime parameter with that same name; choose another name for
234/// user-defined lifetimes.
235///
236#[proc_macro_derive(ReadXDR, attributes(xdr, xdrs))]
237pub fn derive_read_xdr(input: TokenStream) -> TokenStream {
238 let input = syn::parse_macro_input!(input as syn::DeriveInput);
239
240 generate::read_xdr(&input)
241 .unwrap_or_else(syn::Error::into_compile_error)
242 .into()
243}
244
245/// Derives the `XDR` and `HasTypeDef` traits.
246///
247/// This macro generates an implementation that describes the type's XDR
248/// representation, enabling automatic generation of XDR specifications.
249///
250/// # Attributes
251///
252/// - `#[xdr(name = "XdrName")]` - Use a different name in the XDR
253/// specification
254/// - `#[xdr(discrim_type = "TypeName")]` - Specify a custom discriminant type
255/// for enums; the value must be a single XDR identifier (e.g. `"MyEnumType"`),
256/// not a multi-word type expression such as `"unsigned int"`. Passing a
257/// multi-word string causes a panic during schema emission.
258/// - `#[xdr(discrim_var = "field_name")]` - Specify a custom discriminant
259/// field name
260/// - `#[xdr(default_variant)]` - On an enum variant: mark it as the union's
261/// `default:` arm in the generated XDR specification. The variant must be
262/// the trailing variant, must not specify a discriminant, and its first
263/// field must be `i32`.
264///
265/// All three derive macros (`ReadXDR`, `WriteXDR`, `XDR`) accept:
266///
267/// - `#[xdr(crate = "path::to::xdrs")]` - Override the path to the `xdrs` crate
268/// (useful when re-exporting from another crate).
269///
270/// # Limitations
271///
272/// - **Raw identifiers produce invalid XDR IDL.** A field declared as
273/// `r#type: u32` emits `r#type` into the schema, which is not a valid XDR
274/// identifier. Use `#[xdr(name = "type_field")]` (or a rename that avoids the
275/// reserved Rust keyword entirely) to give the field a valid XDR name.
276///
277/// - **Reference fields support only one indirection layer.** `xdr-repr`
278/// implements `HasTypeSpec` for `&T` and `&mut T`, but not for `&&T` or
279/// deeper nesting. A struct field of type `&&u32` or `&&bool` compiles for
280/// `WriteXDR` (which delegates recursively) but fails to derive `XDR` schema
281/// metadata. Flatten the reference to a single `&T` if schema support is
282/// needed.
283///
284/// - **Const-generic monomorphs share the XDR type name.** When a struct
285/// contains two fields of the same generic wrapper type with different const
286/// bounds — for example `Packet<4>` and `Packet<8>` — the generated XDR
287/// schema names both parent fields `Packet`, creating a collision. Define
288/// distinct concrete type aliases (`type Packet4 = Packet<4>;`) so each
289/// monomorph gets its own XDR name.
290///
291#[proc_macro_derive(XDR, attributes(xdr, xdrs))]
292pub fn derive_xdr(input: TokenStream) -> TokenStream {
293 let input = syn::parse_macro_input!(input as syn::DeriveInput);
294
295 generate_type::xdr(&input)
296 .unwrap_or_else(syn::Error::into_compile_error)
297 .into()
298}
299
300/// Derives const-compatible XDR encoding methods for compile-time use.
301///
302/// Generates four inherent `const fn` methods on the type:
303/// - `const_write_cxs<const N: usize>(&self, w: &mut cxs::ConstWriter<N>)`
304/// - `const_write_txs<const N: usize>(&self, w: &mut txs::ConstWriter<N>)`
305/// - `const_cxs_size(&self) -> usize`
306/// - `const_txs_size(&self) -> usize`
307///
308/// These methods can be used in `const` blocks to encode values at compile time.
309/// The size methods allow the buffer size to be derived automatically:
310///
311/// ```rust,ignore
312/// static ENCODED: [u8; MY_VAL.const_txs_size()] = {
313/// let mut w = xdrs::txs::ConstWriter::<{ MY_VAL.const_txs_size() }>::new();
314/// MY_VAL.const_write_txs(&mut w);
315/// w.into_bytes()
316/// };
317/// ```
318///
319/// For worst-case size bounds over all values of a type (buffer sizing
320/// before a value is known), see `#[derive(ConstXDRSize)]`.
321///
322/// # Limitations
323///
324/// - Does not support `#[xdr(into_type)]` or `#[xdr(via)]` attributes on the
325/// derive input, struct fields, or enum variant fields. The `via`/`into_type`
326/// conversions require `Into::into()` which is not `const fn`.
327/// - Supports lifetime and const generics, but not type parameters
328/// - Field types may use recursive concrete shapes inline, including `Option<T>`,
329/// `Result<T, E>`, `&[T]`, `[T; N]`, `Box<T>`, `VecM<T, MAX>`, `BytesM<MAX>`,
330/// `StringM<MAX>`, and tuples, as long as leaf types are concrete and
331/// const-encodable
332/// - With the `alloc` feature, non-empty `VecM`/`BytesM`/`StringM` values
333/// cannot be constructed in const contexts (const heap allocation is not
334/// possible), so const encoding of non-empty values of these types is only
335/// available in no-`alloc` builds, where they wrap `&'static [T]`
336/// - Likewise `Box<T>` values can only be constructed in const contexts in
337/// no-`alloc` builds, where `Box::new` is const and the value is inline;
338/// with `alloc` the generated methods still compile and run at runtime
339///
340/// # Generated code hygiene
341///
342/// The generated `const fn` methods are syntax-directed: type recognition uses
343/// raw identifiers rather than Rust's resolved types, and generated code emits
344/// unqualified names into the call site scope. This means:
345///
346/// - **Use bare primitive names.** `u32` is recognized; `core::primitive::u32`
347/// is not and will be treated as a named leaf type.
348/// - **Do not shadow built-in types.** A local `type u8 = MyType;` will cause
349/// the opaque byte fast-path to misfire. Similarly, shadowing `str` will
350/// confuse string detection.
351/// - Length guards use `::core::assert!()` to resist call-site macro shadowing.
352/// - **Do not use `__N`-prefixed names in discriminant expressions.** The
353/// generated methods introduce a buffer-size const generic with an
354/// `__N`-prefixed name; discriminant expressions referencing a same-named
355/// constant will resolve to the wrong binding.
356/// - **Avoid naming const generics `i`, `arr`, `slice`, or `total`.** The
357/// generated array/slice iteration uses these as local bindings, and Rust
358/// does not allow a `let` binding to shadow an in-scope const parameter.
359/// - **Avoid naming modules `cxs` or `txs` in discriminant paths.** The
360/// generated const block imports `use xdrs::cxs; use xdrs::txs;`, which can
361/// capture discriminant expressions whose paths start with those segments.
362///
363/// # Standard library type recognition
364///
365/// The macro recognizes `Option`, `Result`, `Box`, `PhantomData`, `Infallible`,
366/// and the `xdr_common` container types `VecM`, `BytesM`, and `StringM` by their
367/// **last path segment** (e.g. both `Option<T>` and `std::option::Option<T>`
368/// match). This is a syntactic check — type aliases are not resolved. A field
369/// declared as `type MyOpt = Option<Foo>; .. my_field: MyOpt` will **not** be
370/// recognized as an `Option` and will instead be treated as a named leaf whose
371/// type must provide its own `const_cxs_size` / `const_write_cxs` / etc. inherent
372/// methods (i.e. it must itself derive `ConstWriteXDR` or provide them manually).
373/// The same applies to `PhantomData` — if you need a type-alias, wrap it in a
374/// newtype and derive `ConstWriteXDR` on the newtype instead.
375///
376#[proc_macro_derive(ConstWriteXDR, attributes(xdr, xdrs))]
377pub fn derive_const_write_xdr(input: TokenStream) -> TokenStream {
378 let input = syn::parse_macro_input!(input as syn::DeriveInput);
379
380 generate_const::const_write_xdr(&input)
381 .unwrap_or_else(syn::Error::into_compile_error)
382 .into()
383}
384
385/// Derives worst-case XDR size bounds as `cxs::ConstMaxSize` and
386/// `txs::ConstMaxSize` trait implementations.
387///
388/// The associated const `MAX_SIZE` is the worst-case encoded size over
389/// **all** values of the type — a bound suitable for sizing a buffer before
390/// a value is known — or `None` when no usable bound exists:
391///
392/// ```rust,ignore
393/// #[derive(xdrs::ConstXDRSize)]
394/// struct Frame {
395/// kind: u32,
396/// payload: [u8; 32],
397/// }
398///
399/// const BOUND: usize = match <Frame as xdrs::txs::ConstMaxSize>::MAX_SIZE {
400/// Some(n) => n,
401/// None => panic!("unbounded"),
402/// };
403/// let buf = [0u8; BOUND];
404/// ```
405///
406/// Every field bounds through the `ConstMaxSize` trait — there is no
407/// syntactic type recognition. A field's type participates by implementing
408/// the traits: user types derive `ConstXDRSize`; `xdrs` provides impls for
409/// primitives, `[u8; N]`, `[T; N]`, references, `Option`, `Result`, tuples
410/// (arity ≤ 12), `PhantomData`, `Infallible`, `VecM`/`BytesM`/`StringM`, and
411/// (with the `heapless` feature) `heapless::Vec` / `heapless::String`;
412/// foreign types can implement the traits manually.
413///
414/// Bounds fold through by-value layout only; boundedness does not cross
415/// pointers. References (`&T`, `&[T]`, `&str`) and the heap-backed
416/// element-generic `VecM<T, MAX>` bound to a literal `None` that never names
417/// the pointee's bound, which is what makes const-evaluation cycles
418/// impossible for recursive types: recursion must pass through indirection,
419/// and every indirection bounds to `None`. Recursive types therefore derive
420/// cleanly and report `None`.
421///
422/// Enums bound to the maximum over their variants. All arithmetic is
423/// checked: a bound that overflows `usize` (possible on 32-bit targets for
424/// `u32::MAX`-bounded containers) reports `None` rather than failing
425/// compilation.
426///
427/// # Limitations
428///
429/// - Does not support `#[xdr(into_type)]` or `#[xdr(via)]` attributes.
430/// - Supports lifetime and const generics, but not type parameters.
431/// - Enums require `#[repr(i32)]`, matching the other XDR derives.
432/// - Avoid naming modules `cxs` or `txs` in discriminant paths: the
433/// generated const block imports `use xdrs::cxs; use xdrs::txs;`.
434///
435/// All derives accept `#[xdr(crate = "path")]` to override the `::xdrs`
436/// import path.
437#[proc_macro_derive(ConstXDRSize, attributes(xdr, xdrs))]
438pub fn derive_const_xdr_size(input: TokenStream) -> TokenStream {
439 let input = syn::parse_macro_input!(input as syn::DeriveInput);
440
441 generate_const::const_xdr_size(&input)
442 .unwrap_or_else(syn::Error::into_compile_error)
443 .into()
444}