[PATCH v3 2/4] ynl_gen: generate Rust files from yaml files

Alice Ryhl
2026-04-15T09:39:36Z

To generate netlink frames from Rust code easily, generate Rust
libraries with methods for generating different netlink messages as
appropriate.

The new 'rust' type corresponds to a Rust version of the C target
'kernel'. There is no Rust version of the 'uapi' target since Rust code
exports its uapi via C headers - choice of language is opaque to
userspace.

This logic is kept in the existing ynl_gen_c.py file to reuse CodeWriter
and other shared pieces of logic in the existing python file. This has
the disadvantage that the gen_c part of the name is now wrong, as it
also generates Rust. One possible solution to this could be to rename
the file.

Signed-off-by: Alice Ryhl <aliceryhl@google.com>
---
tools/net/ynl/pyynl/ynl_gen_c.py | 139 ++++++++++++++++++++++++++++++++++++++-
tools/net/ynl/ynl-regen.sh | 2 +-
2 files changed, 139 insertions(+), 2 deletions(-)

diff --git a/tools/net/ynl/pyynl/ynl_gen_c.py b/tools/net/ynl/pyynl/ynl_gen_c.py
index 0e1e486c1185..76b8b2f1ac16 100755
--- a/tools/net/ynl/pyynl/ynl_gen_c.py
+++ b/tools/net/ynl/pyynl/ynl_gen_c.py
@@ -19,6 +19,7 @@ import pathlib
import os
import re
import shutil
+import subprocess
import sys
import tempfile
import yaml as pyyaml
@@ -1744,6 +1745,19 @@ class CodeWriter:
else:
self.p('}' + line)

+ def array_start(self, line=''):
+ if line:
+ line = line + ' '
+ self.p(line + '[')
+ self._ind += 1
+
+ def array_end(self, line=''):
+ if line and line[0] not in {';', ','}:
+ line = ' ' + line
+ self._ind -= 1
+ self._nl = False
+ self.p(']' + line)
+
def write_doc_line(self, doc, indent=True):
words = doc.split()
line = ' *'
@@ -3415,10 +3429,126 @@ def find_kernel_root(full_path):
return full_path, sub_path[:-1]

+def render_rust(family, cw):
+ cw.p('#![allow(unreachable_pub, clippy::wrong_self_convention)]')
+ cw.p('use kernel::netlink::{Family, MulticastGroup};')
+ cw.p('use kernel::prelude::*;')
+ cw.nl()
+
+ family_upper = c_upper(family.ident_name)
+ family_name = f'{family_upper}_NL_FAMILY'
+ mcgrps_name = f'{family_name}_MCGRPS'
+
+ cw.p(f'pub static {family_name}: Family = Family::const_new(')
+ cw._ind += 1
+ cw.p('&crate::THIS_MODULE,')
+ cw.p(f'kernel::uapi::{family.fam_key},')
+ cw.p(f'kernel::uapi::{family.ver_key},')
+ if family.mcgrps['list']:
+ cw.p(f'&{mcgrps_name},')
+ else:
+ cw.p('&[],')
+ cw._ind -= 1
+ cw.p(');')
+ cw.nl()
+
+ if family.mcgrps['list']:
+ cw.array_start(f'static {mcgrps_name}: [MulticastGroup; {len(family.mcgrps["list"])}] = ')
+ for grp in family.mcgrps['list']:
+ cw.p(f'MulticastGroup::const_new(c"{grp["name"]}"),')
+ cw.array_end(';')
+ cw.nl()
+
+ for idx, (op_name, op) in enumerate(item for item in family.msgs.items() if 'event' in item[1]):
+ struct_name = op_name.capitalize()
+
+ if 'doc' in op:
+ doc_lines = op['doc'].strip().split('\n')
+ for line in doc_lines:
+ cw.p(f'/// {line.strip()}')
+
+ cw.block_start(f'pub struct {struct_name}')
+ cw.p('skb: kernel::netlink::GenlMsg,')
+ cw.block_end()
+ cw.nl()
+
+ cw.block_start(f'impl {struct_name}')
+ cw.p('/// Create a new multicast message.')
+ cw.p('pub fn new(')
+ cw._ind += 1
+ cw.p('size: usize,')
+ cw.p('portid: u32,')
+ cw.p('seq: u32,')
+ cw.p('flags: kernel::alloc::Flags,')
+ cw._ind -= 1
+ cw.block_start(') -> Result<Self, kernel::alloc::AllocError>')
+ cw.p(f'const {op.enum_name}: u8 = kernel::uapi::{op.enum_name} as u8;')
+ cw.p('let skb = kernel::netlink::NetlinkSkBuff::new(size, flags)?;')
+ cw.p(f'let skb = skb.genlmsg_put(portid, seq, &{family_name}, {op.enum_name})?;')
+ cw.p('Ok(Self { skb })')
+ cw.block_end()
+ cw.nl()
+
+ grp_idx = 0
+ if 'mcgrp' in op:
+ grp_idx = next(i for i, grp in enumerate(family.mcgrps['list']) if grp['name'] == op['mcgrp'])
+
+ cw.p('/// Broadcast this message.')
+ cw.block_start('pub fn multicast(self, portid: u32, flags: kernel::alloc::Flags) -> Result')
+ cw.p(f'self.skb.multicast(&{family_name}, portid, {grp_idx}, flags)')
+ cw.block_end()
+ cw.nl()
+
+ cw.p('/// Check if this message type has listeners.')
+ cw.block_start('pub fn has_listeners() -> bool')
+ cw.p(f'{family_name}.has_listeners({grp_idx})')
+ cw.block_end()
+
+ attr_set_name = op['attribute-set']
+ attr_set = family.attr_sets[attr_set_name]
+ event_attrs = op['event']['attributes']
+
+ for attr_name in event_attrs:
+ attr = attr_set[attr_name]
+ method_name = attr_name.replace('-', '_')
+
+ if attr.type == 'u32':
+ put_fn = 'put_u32'
+ arg_str = ', val'
+ method_args = '(&mut self, val: u32)'
+ elif attr.type == 'string':
+ put_fn = 'put_string'
+ arg_str = ', val'
+ method_args = '(&mut self, val: &CStr)'
+ elif attr.type == 'flag':
+ put_fn = 'put_flag'
+ arg_str = ''
+ method_args = '(&mut self)'
+ else:
+ put_fn = f'put_{attr.type}'
+ arg_str = ', val'
+ method_args = f'(&mut self, val: {attr.type})'
+
+ cw.nl()
+ if 'doc' in attr.yaml:
+ doc_lines = attr.yaml['doc'].strip().split('\n')
+ for line in doc_lines:
+ cw.p(f'/// {line.strip()}')
+
+ cw.block_start(f'pub fn {method_name}{method_args} -> Result')
+ cw.p(f'const {attr.enum_name}: c_int = kernel::uapi::{attr.enum_name} as c_int;')
+ cw.p(f'self.skb.{put_fn}({attr.enum_name}{arg_str})')
+ cw.block_end()
+
+ cw.block_end()
+ cw.nl()
+ cw.p(' ')
+
+
def main():
parser = argparse.ArgumentParser(description='Netlink simple parsing generator')
parser.add_argument('--mode', dest='mode', type=str, required=True,
- choices=('user', 'kernel', 'uapi'))
+ choices=('user', 'kernel', 'uapi', 'rust'))
parser.add_argument('--spec', dest='spec', type=str, required=True)
parser.add_argument('--header', dest='header', action='store_true', default=None)
parser.add_argument('--source', dest='header', action='store_false')
@@ -3471,6 +3601,13 @@ def main():
render_uapi(parsed, cw)
return

+ if args.mode == 'rust':
+ render_rust(parsed, cw)
+ cw.close_out_file()
+ if args.out_file:
+ subprocess.run(['rustfmt', '--edition', '2021', args.out_file])
+ return
+
hdr_prot = f"_LINUX_{parsed.c_name.upper()}_GEN_H"
if args.header:
cw.p('#ifndef ' + hdr_prot)
diff --git a/tools/net/ynl/ynl-regen.sh b/tools/net/ynl/ynl-regen.sh
index d9809276db98..4f5ceb4fe147 100755
--- a/tools/net/ynl/ynl-regen.sh
+++ b/tools/net/ynl/ynl-regen.sh
@@ -17,7 +17,7 @@ done
KDIR=$(dirname $(dirname $(dirname $(dirname $(realpath $0)))))
pushd ${search:-$KDIR} >>/dev/null

-files=$(git grep --files-with-matches '^/\* YNL-GEN \(kernel\|uapi\|user\)')
+files=$(git grep --files-with-matches '^/\* YNL-GEN \(kernel\|uapi\|user\|rust\)')
for f in $files; do
# params: 0 1 2 3
# $YAML YNL-GEN kernel $mode

--
2.54.0.rc0.605.g598a273b03-goog