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