Skip to main content

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}