Skip to main content

nlink_macros/
lib.rs

1//! Proc-macro derives for the [`nlink`][nlink] Linux-netlink
2//! library — typed GENL family / command / attribute / message
3//! codecs.
4//!
5//! **Don't depend on this crate directly.** Use `nlink` and write
6//! `use nlink::macros::*;` to pull in every macro plus the
7//! supporting traits in one shot. See the README + the
8//! [`define-your-own-genl-family` recipe][recipe] for end-to-end
9//! usage; the runnable example lives at
10//! [`crates/nlink/examples/macros/define_taskstats.rs`][example].
11//!
12//! # Shipped surface (0.16, Plan 154 Phases 1–6)
13//!
14//! - [`macro@GenlCommand`] + `#[genl_command(repr = "u8"|"u16")]`
15//!   — typed GENL command enum
16//! - [`macro@GenlAttribute`] + `#[genl_attribute(repr = "u8"|"u16")]`
17//!   — typed attribute-kind enum
18//! - [`macro@GenlEnum`] + `#[genl_enum(repr = "u8"|"u16"|"u32")]`
19//!   — typed value enum encoded *inside* an attribute payload
20//! - [`macro@GenlMessage`] + `#[genl_message(cmd = ...)]` +
21//!   per-field `#[genl_attr(...)]` — typed request/response
22//!   body. Pairs with the generic
23//!   `Connection::<F: GenlFamily>::send_typed<M, R>` /
24//!   `dump_typed_stream<M, R>` dispatch in `nlink`.
25//! - [`macro@genl_family`] — `#[genl_family(name = "...", version
26//!   = N)]` rewrites a unit-struct declaration into a complete
27//!   family marker (`ProtocolState` + `AsyncProtocolInit` +
28//!   `GenlFamily` + sealed-trait impls + `family_id` field +
29//!   `Default` / `Debug`).
30//!
31//! The three codec derives (`GenlCommand`, `GenlAttribute`,
32//! `GenlEnum`) produce the same shape — `From<EnumType> for
33//! ReprType` + `TryFrom<ReprType> for EnumType` + a small
34//! `EnumTypeUnknownValue(repr)` error newtype.
35//!
36//! # Deferred follow-up
37//!
38//! `#[derive(NetlinkAttrs)]` for nested attribute groups
39//! (`NLA_F_NESTED`) is tracked as a Plan 154 follow-up. The
40//! `nlink::macros::NetlinkAttrs` *trait* is already in tree so
41//! hand-implementations work today; the derive will close the
42//! last remaining "hand-roll this bit" gap.
43//!
44//! [nlink]: https://docs.rs/nlink
45//! [recipe]: https://github.com/p13marc/nlink/blob/master/docs/recipes/define-your-own-genl-family.md
46//! [example]: https://github.com/p13marc/nlink/blob/master/crates/nlink/examples/macros/define_taskstats.rs
47//!
48//! Doc examples in this crate are ```` ```text ````, not compiled: they show
49//! the derives being *applied*, which needs both this crate and `nlink` in
50//! scope, and `nlink-macros` cannot depend on `nlink` without a cycle. The
51//! compiled versions live in `nlink`'s own documentation and in
52//! `crates/nlink/examples/macros/define_taskstats.rs`.
53
54use proc_macro::TokenStream;
55use proc_macro2::Span;
56use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Lit, LitStr, Meta};
57
58// Re-export the syn `ItemStruct` shape used by the
59// `#[genl_family]` attribute macro to consume its input.
60pub(crate) use syn::ItemStruct;
61
62mod codec;
63mod genl_attribute;
64mod genl_command;
65mod genl_family;
66mod genl_enum;
67mod genl_message;
68mod netlink_attrs;
69
70/// Derive a typed-enum codec for a Generic Netlink **command** ID
71/// enum.
72///
73/// Generates `impl From<EnumType> for ReprType` (infallible —
74/// every variant has a known discriminant) and `impl
75/// TryFrom<ReprType> for EnumType` (fallible — unknown values
76/// land in an `Err(InvalidValue)` arm).
77///
78/// `ReprType` is either `u8` or `u16` per the
79/// `#[genl_command(repr = "...")]` attribute.
80///
81/// # Example
82///
83/// ```text
84/// use nlink_macros::GenlCommand;
85///
86/// #[derive(GenlCommand, Debug, Clone, Copy, PartialEq, Eq)]
87/// #[genl_command(repr = "u8")]
88/// #[non_exhaustive]
89/// pub enum MyCmd {
90///     Unspec = 0,
91///     Get = 1,
92///     Set = 2,
93/// }
94///
95/// // Generated impls:
96/// let raw: u8 = MyCmd::Get.into();
97/// assert_eq!(raw, 1);
98/// let parsed = MyCmd::try_from(2u8).unwrap();
99/// assert_eq!(parsed, MyCmd::Set);
100/// assert!(MyCmd::try_from(255u8).is_err());
101/// ```
102///
103/// # Requirements
104///
105/// - The annotated type must be an `enum`.
106/// - Each variant must have an explicit `= literal` discriminant
107///   (e.g. `Get = 1`). Anonymous-discriminant variants (`Get,`)
108///   are rejected at compile time because kernel ABI demands
109///   stable wire values.
110/// - Variants must be unit-only (no fields). Tuple/struct
111///   variants are rejected.
112///
113/// Errors point at the offending span via `syn::Error::new_spanned`.
114#[proc_macro_derive(GenlCommand, attributes(genl_command))]
115pub fn derive_genl_command(input: TokenStream) -> TokenStream {
116    let input = parse_macro_input!(input as DeriveInput);
117    genl_command::expand(input)
118        .unwrap_or_else(|e| e.to_compile_error())
119        .into()
120}
121
122/// Derive a typed-enum codec for a Generic Netlink **attribute
123/// kind** enum.
124///
125/// Same shape as [`macro@GenlCommand`] but for the u16
126/// attribute-type field on each `nlattr`. Accepts `repr = "u8"`
127/// or `repr = "u16"`; the `NLA_F_NESTED` (0x8000) and
128/// `NLA_F_NET_BYTEORDER` (0x4000) flag bits are the caller's
129/// responsibility — the derive doesn't reserve them.
130///
131/// # Example
132///
133/// ```text
134/// use nlink_macros::GenlAttribute;
135///
136/// #[derive(GenlAttribute, Debug, Clone, Copy, PartialEq, Eq)]
137/// #[genl_attribute(repr = "u16")]
138/// #[non_exhaustive]
139/// pub enum DpllAttr {
140///     Id = 1,
141///     ModuleName = 2,
142///     ClockId = 4,
143///     Mode = 5,
144/// }
145/// ```
146#[proc_macro_derive(GenlAttribute, attributes(genl_attribute))]
147pub fn derive_genl_attribute(input: TokenStream) -> TokenStream {
148    let input = parse_macro_input!(input as DeriveInput);
149    genl_attribute::expand(input)
150        .unwrap_or_else(|e| e.to_compile_error())
151        .into()
152}
153
154/// Derive a typed-enum codec for a value enum encoded *inside*
155/// an attribute payload (rather than as the attribute kind
156/// itself).
157///
158/// Use this for kernel-UAPI enums like `DPLL_LOCK_STATUS_*`,
159/// `DEVLINK_RATE_TYPE_*`, or any other typed value the kernel
160/// declares via `enum dpll_lock_status` etc. Accepts `repr =
161/// "u8"`, `"u16"`, or `"u32"`. Strictly weaker than
162/// [`macro@GenlAttribute`] — no attribute-kind machinery — and
163/// no constraint on 1-based-vs-0-based discriminants: the derive
164/// matches whatever the user declares.
165///
166/// # Example
167///
168/// ```text
169/// use nlink_macros::GenlEnum;
170///
171/// // 1-based (the common kernel convention).
172/// #[derive(GenlEnum, Debug, Clone, Copy, PartialEq, Eq)]
173/// #[genl_enum(repr = "u32")]
174/// #[non_exhaustive]
175/// pub enum DpllMode {
176///     Manual = 1,
177///     Automatic = 2,
178/// }
179///
180/// // 0-based outlier (rare but real — DPLL_FEATURE_STATE_*).
181/// #[derive(GenlEnum, Debug, Clone, Copy, PartialEq, Eq)]
182/// #[genl_enum(repr = "u32")]
183/// #[non_exhaustive]
184/// pub enum DpllFeatureState {
185///     Disable = 0,
186///     Enable = 1,
187/// }
188/// ```
189#[proc_macro_derive(GenlEnum, attributes(genl_enum))]
190pub fn derive_genl_enum(input: TokenStream) -> TokenStream {
191    let input = parse_macro_input!(input as DeriveInput);
192    genl_enum::expand(input)
193        .unwrap_or_else(|e| e.to_compile_error())
194        .into()
195}
196
197/// Derive `nlink::macros::GenlMessage` for a struct-shaped GENL
198/// message body.
199///
200/// The struct's `#[genl_message(cmd = EXPR)]` attribute supplies
201/// the command byte (any compile-time-evaluable expression that
202/// casts to `u8` — typically an integer literal or a typed-enum
203/// variant cast like `cmd = MyCmd::Get`). Each named field carries
204/// a `#[genl_attr(EXPR)]` attribute naming its on-wire attribute
205/// kind (similarly, any expression that casts to `u16`).
206///
207/// # Supported field types (0.16 Phase 3b)
208///
209/// - `u8` / `u16` / `u32` / `u64`
210/// - `String`
211/// - `Vec<u8>`
212/// - `Option<T>` where `T` is any of the above — omitted on
213///   `None`, present-when-`Some`, `Some(parsed)` if the kernel
214///   returns it.
215///
216/// Unsupported types (`i32`, nested groups, `IpAddr`, `bool`)
217/// produce a compile-time error that points at the field.
218/// Nested-group support via `#[derive(NetlinkAttrs)]` ships in a
219/// later phase.
220///
221/// # `from_bytes` semantics
222///
223/// Missing attributes produce default values (zero for ints,
224/// empty for strings/bytes, `None` for `Option<T>`). Unknown
225/// attribute types are silently skipped — forward-compatibility
226/// with newer kernels.
227///
228/// # Example
229///
230/// ```text
231/// use nlink::macros::*;
232///
233/// #[derive(GenlCommand, Debug, Clone, Copy, PartialEq, Eq)]
234/// #[genl_command(repr = "u8")]
235/// pub enum MyCmd { Unspec = 0, Get = 1 }
236///
237/// #[derive(GenlAttribute, Debug, Clone, Copy, PartialEq, Eq)]
238/// #[genl_attribute(repr = "u16")]
239/// pub enum MyAttr { Id = 1, Name = 2, Description = 3 }
240///
241/// #[derive(GenlMessage, Debug)]
242/// #[genl_message(cmd = MyCmd::Get)]
243/// pub struct GetRequest {
244///     #[genl_attr(MyAttr::Id)]
245///     pub id: u32,
246///     #[genl_attr(MyAttr::Name)]
247///     pub name: String,
248///     #[genl_attr(MyAttr::Description)]
249///     pub description: Option<String>,
250/// }
251///
252/// // Generated:
253/// // impl GenlMessage for GetRequest {
254/// //     const CMD: u8 = MyCmd::Get as u8;  // = 1
255/// //     fn to_bytes(&self, b: &mut MessageBuilder) -> Result<()> { ... }
256/// //     fn from_bytes(payload: &[u8]) -> Result<Self> { ... }
257/// // }
258/// ```
259#[proc_macro_derive(GenlMessage, attributes(genl_message, genl_attr))]
260pub fn derive_genl_message(input: TokenStream) -> TokenStream {
261    let input = parse_macro_input!(input as DeriveInput);
262    genl_message::expand(input)
263        .unwrap_or_else(|e| e.to_compile_error())
264        .into()
265}
266
267/// Derive `NetlinkAttrs` for a nested attribute group — a struct
268/// the kernel encodes as the contents of a single `NLA_F_NESTED`
269/// attribute.
270///
271/// Same field-type-mapping table as `#[derive(GenlMessage)]`
272/// (primitives + `Option<T>` + `Vec<u8>` + `Vec<GenlEnum>` +
273/// bitflags + `Option<GenlEnum>`), same per-field
274/// `#[genl_attr(EXPR [, repr = "..."] [, bitflags = "..."])]`
275/// annotation. The only difference: no `cmd` const, methods are
276/// `write_attrs` / `read_attrs` (matching the
277/// `nlink::macros::NetlinkAttrs` trait).
278///
279/// # Example
280///
281/// ```text
282/// use nlink::macros::*;
283///
284/// #[derive(NetlinkAttrs, Debug, Default)]
285/// pub struct ParentDeviceBlock {
286///     #[genl_attr(1u16)] pub device_id: u32,
287///     #[genl_attr(2u16)] pub pin_id: u32,
288/// }
289///
290/// // Use the group inside a GenlMessage struct via `nested`:
291/// #[derive(GenlMessage, Debug, Default)]
292/// #[genl_message(cmd = DpllCmd::PinGet)]
293/// pub struct DpllPinReply {
294///     #[genl_attr(DpllPinAttr::Id)] pub id: u32,
295///     #[genl_attr(DpllPinAttr::ParentDevice, nested)]
296///     pub parent_device: Option<ParentDeviceBlock>,
297/// }
298/// ```
299#[proc_macro_derive(NetlinkAttrs, attributes(genl_attr))]
300pub fn derive_netlink_attrs(input: TokenStream) -> TokenStream {
301    let input = parse_macro_input!(input as DeriveInput);
302    netlink_attrs::expand(input)
303        .unwrap_or_else(|e| e.to_compile_error())
304        .into()
305}
306
307/// Declare a Generic Netlink family marker.
308///
309/// Rewrites a unit-struct declaration into a complete family
310/// marker type with all the trait impls (`ProtocolState`,
311/// `AsyncProtocolInit`, `__macro_seal::ProtocolStateSeal`,
312/// `__macro_seal::AsyncConstructibleSeal`) the rest of nlink
313/// needs to plug the marker into the existing
314/// `Connection::<P>::new_async()` machinery.
315///
316/// # Arguments
317///
318/// - `name = "..."` — the family name registered with the
319///   kernel (matches the `nl80211`, `dpll`, `wireguard` strings).
320/// - `version = N` — the GENL family version (kernel UAPI;
321///   typically `1`).
322///
323/// # Example
324///
325/// ```text
326/// use nlink::macros::genl_family;
327///
328/// #[genl_family(name = "my_family", version = 1)]
329/// pub struct MyFamily;
330///
331/// // Expands to a struct with a `family_id: u16` field +
332/// // `MyFamily::NAME` + `MyFamily::VERSION` constants +
333/// // ProtocolState / AsyncProtocolInit / AsyncConstructible
334/// // impls. Use as the protocol marker:
335/// //
336/// // let conn = Connection::<MyFamily>::new_async().await?;
337/// ```
338///
339/// # Requirements
340///
341/// - The annotated struct must be a unit struct (`pub struct
342///   MyFamily;`). The macro rewrites it to add the `family_id`
343///   field; pre-declared fields would conflict.
344/// - The struct must not be generic.
345///
346/// # Sealed-trait impl detail
347///
348/// The macro emits `impl
349/// nlink::netlink::protocol::__macro_seal::ProtocolStateSeal`
350/// (which is the private `Sealed` trait re-exported under a
351/// `#[doc(hidden)]` path for this macro's use). This satisfies
352/// the in-tree sealed-trait contract that prevents arbitrary
353/// types from claiming `ProtocolState`; using
354/// `#[genl_family]` is the only path the contract authorizes.
355#[proc_macro_attribute]
356pub fn genl_family(args: TokenStream, item: TokenStream) -> TokenStream {
357    let item = parse_macro_input!(item as ItemStruct);
358    genl_family::expand(args.into(), item)
359        .unwrap_or_else(|e| e.to_compile_error())
360        .into()
361}
362
363// --------------------------------------------------------------
364// Shared helpers used across derives. Kept in lib.rs because
365// they're small + private to this crate.
366// --------------------------------------------------------------
367
368/// Width of a typed-codec enum's wire representation.
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub(crate) enum ReprWidth {
371    U8,
372    U16,
373    U32,
374}
375
376impl ReprWidth {
377    pub(crate) fn ident(self) -> proc_macro2::Ident {
378        let s = match self {
379            Self::U8 => "u8",
380            Self::U16 => "u16",
381            Self::U32 => "u32",
382        };
383        proc_macro2::Ident::new(s, Span::call_site())
384    }
385
386    /// Parse from the `repr = "u8"` string-literal form used by
387    /// `#[genl_command(repr = "...")]` and siblings.
388    pub(crate) fn parse(lit: &LitStr) -> syn::Result<Self> {
389        match lit.value().as_str() {
390            "u8" => Ok(Self::U8),
391            "u16" => Ok(Self::U16),
392            "u32" => Ok(Self::U32),
393            other => Err(syn::Error::new(
394                lit.span(),
395                format!("unknown repr {other:?}; expected \"u8\", \"u16\", or \"u32\""),
396            )),
397        }
398    }
399}
400
401/// Find the `#[genl_command(...)]` (or other named) attribute on
402/// the derive input and return its `Meta::List`.
403pub(crate) fn find_meta_list<'a>(
404    attrs: &'a [syn::Attribute],
405    name: &str,
406) -> Option<&'a syn::MetaList> {
407    attrs.iter().find_map(|a| match &a.meta {
408        Meta::List(ml) if ml.path.is_ident(name) => Some(ml),
409        _ => None,
410    })
411}
412
413/// Parse `repr = "u8"` (etc.) out of the `Meta::List` inside an
414/// attribute. Returns `Err` if `repr` is missing or malformed.
415pub(crate) fn parse_repr_attr(ml: &syn::MetaList, attr_name: &str) -> syn::Result<ReprWidth> {
416    let mut found_repr: Option<ReprWidth> = None;
417    ml.parse_nested_meta(|meta| {
418        if meta.path.is_ident("repr") {
419            let value = meta.value()?;
420            let lit: LitStr = value.parse()?;
421            found_repr = Some(ReprWidth::parse(&lit)?);
422            Ok(())
423        } else {
424            Err(meta.error(format!(
425                "unknown {attr_name} key {:?}; expected `repr`",
426                meta.path
427                    .get_ident()
428                    .map(|i| i.to_string())
429                    .unwrap_or_default()
430            )))
431        }
432    })?;
433    found_repr.ok_or_else(|| {
434        syn::Error::new_spanned(
435            ml,
436            format!("#[{attr_name}(...)] must specify `repr = \"u8\"|\"u16\"|\"u32\"`"),
437        )
438    })
439}
440
441/// Extract the explicit `= literal` discriminant from a variant.
442/// Returns the literal value as a `u64` (variants must fit; the
443/// derive validates against the repr's width separately).
444pub(crate) fn variant_discriminant(variant: &syn::Variant) -> syn::Result<u64> {
445    let (_, expr) = variant.discriminant.as_ref().ok_or_else(|| {
446        syn::Error::new_spanned(
447            variant,
448            "GenlCommand/GenlAttribute/GenlEnum variants must have an \
449             explicit `= literal` discriminant — kernel ABI requires \
450             stable wire values",
451        )
452    })?;
453    match expr {
454        Expr::Lit(ExprLit {
455            lit: Lit::Int(int), ..
456        }) => int.base10_parse::<u64>(),
457        _ => Err(syn::Error::new_spanned(
458            expr,
459            "discriminant must be an integer literal (e.g., `= 1`)",
460        )),
461    }
462}
463
464/// Ensure the data is an enum + every variant is a unit variant
465/// (no fields).
466pub(crate) fn require_unit_enum<'a>(
467    data: &'a Data,
468    derive_name: &str,
469    span: Span,
470) -> syn::Result<&'a syn::DataEnum> {
471    let de = match data {
472        Data::Enum(e) => e,
473        Data::Struct(_) => {
474            return Err(syn::Error::new(
475                span,
476                format!("#[derive({derive_name})] is only valid on enums, not structs"),
477            ))
478        }
479        Data::Union(_) => {
480            return Err(syn::Error::new(
481                span,
482                format!("#[derive({derive_name})] is only valid on enums, not unions"),
483            ))
484        }
485    };
486    for v in &de.variants {
487        match &v.fields {
488            syn::Fields::Unit => {}
489            _ => {
490                return Err(syn::Error::new_spanned(
491                    v,
492                    format!(
493                        "#[derive({derive_name})] variants must be unit-only \
494                         (no fields); `{}` has fields",
495                        v.ident
496                    ),
497                ))
498            }
499        }
500    }
501    Ok(de)
502}
503
504/// Validate that `value` fits in `width` (the discriminant doesn't
505/// overflow the chosen repr).
506pub(crate) fn fits_in_width(value: u64, width: ReprWidth) -> bool {
507    match width {
508        ReprWidth::U8 => value <= u8::MAX as u64,
509        ReprWidth::U16 => value <= u16::MAX as u64,
510        ReprWidth::U32 => value <= u32::MAX as u64,
511    }
512}
513