[PATCH 6/8] rust: macros: add early_param support to module! macro

From: Matthew Wood

Date: Thu Feb 26 2026 - 18:51:07 EST


Allow module parameters to be registered as early boot command-line
parameters via the existing __setup infrastructure.

Add an optional `early_param` field to the parameter block in the
module! macro. When specified, the macro generates:

- A setup callback that calls ModuleParam::from_setup_arg() and
stores the result via ModuleParamAccess::set_value().
- A static string in .init.rodata with the command-line prefix.
- An ObsKernelParam entry in the .init.setup section linking the
two together.

The generated code is gated behind #[cfg(not(MODULE))] since __setup
parameters are only meaningful for built-in modules — loadable modules
receive their parameters through the standard module_param mechanism.

Signed-off-by: Matthew Wood <thepacketgeek@xxxxxxxxx>
---
rust/macros/lib.rs | 5 +++
rust/macros/module.rs | 81 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 86 insertions(+)

diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
index 0c36194d9971..83dcc89a425a 100644
--- a/rust/macros/lib.rs
+++ b/rust/macros/lib.rs
@@ -35,9 +35,14 @@
/// parameter_name: type {
/// default: default_value,
/// description: "Description",
+/// early_param: "cmdline_prefix=", // optional
/// }
/// ```
///
+/// The optional `early_param` field registers the parameter as an early
+/// boot command-line parameter via `__setup`. The value is the command-line
+/// prefix string (e.g. `"netconsole="`).
+///
/// `type` may be one of
///
/// - [`i8`]
diff --git a/rust/macros/module.rs b/rust/macros/module.rs
index 0d76743741fb..4d2e144fa6de 100644
--- a/rust/macros/module.rs
+++ b/rust/macros/module.rs
@@ -38,6 +38,7 @@ struct ModInfoBuilder<'a> {
counter: usize,
ts: TokenStream,
param_ts: TokenStream,
+ setup_ts: TokenStream,
}

impl<'a> ModInfoBuilder<'a> {
@@ -47,6 +48,7 @@ fn new(module: &'a str) -> Self {
counter: 0,
ts: TokenStream::new(),
param_ts: TokenStream::new(),
+ setup_ts: TokenStream::new(),
}
}

@@ -204,6 +206,78 @@ fn emit_params(&mut self, info: &ModuleInfo) {
});
}
}
+
+ fn emit_setup(&mut self, info: &ModuleInfo) {
+ let Some(params) = &info.params else {
+ return;
+ };
+
+ for param in params {
+ let Some(early_param) = &param.early_param else {
+ continue;
+ };
+
+ let setup_str_value = early_param.value();
+ let param_name = &param.name;
+
+ // Resolve `string` shorthand to the real `StringParam` type.
+ let param_type_str = param.ptype.to_token_stream().to_string();
+ let actual_type: Type = if param_type_str == "string" {
+ parse_quote!(::kernel::module_param::StringParam)
+ } else {
+ param.ptype.clone()
+ };
+
+ // Create identifiers for the generated statics
+ let setup_fn_name = format_ident!("__setup_fn_{}", param_name);
+ let setup_str_name =
+ format_ident!("__SETUP_STR_{}", param_name.to_string().to_uppercase());
+ let setup_name = format_ident!("__SETUP_{}", param_name.to_string().to_uppercase());
+
+ // The setup string with null terminator
+ let setup_str_bytes = format!("{}\0", setup_str_value);
+ let setup_str_len = setup_str_bytes.len();
+ let setup_str_lit = Literal::byte_string(setup_str_bytes.as_bytes());
+
+ self.setup_ts.extend(quote! {
+ #[cfg(not(MODULE))]
+ const _: () = {
+ unsafe extern "C" fn #setup_fn_name(
+ val: *mut ::kernel::ffi::c_char,
+ ) -> ::kernel::ffi::c_int {
+ if val.is_null() {
+ return 0;
+ }
+ // SAFETY: The kernel passes a valid null-terminated string from
+ // `static_command_line`.
+ match unsafe {
+ <#actual_type as ::kernel::module_param::ModuleParam>::from_setup_arg(
+ val as *const _
+ )
+ } {
+ Ok(v) => {
+ if module_parameters::#param_name.set_value(v) { 1 } else { 0 }
+ }
+ Err(_) => 0,
+ }
+ }
+
+ #[link_section = ".init.rodata"]
+ #[used(compiler)]
+ static #setup_str_name: [u8; #setup_str_len] = *#setup_str_lit;
+
+ #[link_section = ".init.setup"]
+ #[used(compiler)]
+ static #setup_name: ::kernel::module_param::ObsKernelParam =
+ ::kernel::module_param::ObsKernelParam {
+ str_: #setup_str_name.as_ptr().cast(),
+ setup_func: ::core::option::Option::Some(#setup_fn_name),
+ early: 0,
+ };
+ };
+ });
+ }
+ }
}

fn param_ops_path(param_type: &str) -> Path {
@@ -367,6 +441,7 @@ struct Parameter {
ptype: Type,
default: Expr,
description: LitStr,
+ early_param: Option<LitStr>,
}

impl Parse for Parameter {
@@ -382,6 +457,7 @@ fn parse(input: ParseStream<'_>) -> Result<Self> {
from fields;
default [required] => fields.parse()?,
description [required] => fields.parse()?,
+ early_param => fields.parse()?,
}

Ok(Self {
@@ -389,6 +465,7 @@ fn parse(input: ParseStream<'_>) -> Result<Self> {
ptype,
default,
description,
+ early_param,
})
}
}
@@ -501,9 +578,11 @@ pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
modinfo.emit_only_builtin("file", &file, false);

modinfo.emit_params(&info);
+ modinfo.emit_setup(&info);

let modinfo_ts = modinfo.ts;
let params_ts = modinfo.param_ts;
+ let setup_ts = modinfo.setup_ts;

let ident_init = format_ident!("__{ident}_init");
let ident_exit = format_ident!("__{ident}_exit");
@@ -678,5 +757,7 @@ unsafe fn __exit() {
mod module_parameters {
#params_ts
}
+
+ #setup_ts
})
}
--
2.52.0