public inbox for rust-for-linux@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 00/11] refactor Rust proc macros with `syn`
@ 2026-01-07 16:15 Gary Guo
  2026-01-07 16:15 ` [PATCH v2 01/11] rust: pin-init: internal: remove proc-macro[2] and quote workarounds Gary Guo
                   ` (10 more replies)
  0 siblings, 11 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich
  Cc: rust-for-linux

From: Gary Guo <gary@garyguo.net>

This series convert Rust proc macros that we have with `syn`, and replace the
custom `quote!` macro that we have with the vendored `quote!` macro. The
`pin-init` macros are not converted `syn` yet; Benno has a work in progress in
converting them. They're however converted to use `quote` and `proc-macro2`
crate so our custom `quote!` macro can be removed.

Overall this improves the robustness of the macros as we have precise parsing of
the AST rather than relying on heuristics to extract needed information from
there. This is also a quality-of-life improvement to those using language
servers (e.g. Rust analyzer) as the span information of the proc macros are now
preserved which allows the "jump-to-definition" feature to work, even when used
on completely custom macros such as `module!`.

Miguel gave a very good explaination on why `syn` is a good idea in the patch
series that introduced it [1], which I shall not repeat here.

Link: https://lore.kernel.org/rust-for-linux/20251124151837.2184382-1-ojeda@kernel.org/ [1]

Changes since v1:
- Fixed clippy warnings when moving from `proc-macro` to `proc-macro2`
  (extra trait impl in `proc-macro2` causes some `.to_string()` to be
  unnecessary).
- A Makefile change to pin-init-internal that is mistakenly included in
  patch 5/11 is moved to the correct patch 1/11.
- `module!` parsing has been completely reworked to support `imports_ns`
  and `params`. As there're two structs that need to parse ordered fields
  (`ModuleInfo` and `Parameter`), a `parse_ordered_field` macro is added
  and it is used to implement the parsing instead of `ModInfoField` and
  custom keywords.
- Link to v1: https://lore.kernel.org/rust-for-linux/20251211185805.2835633-1-gary@kernel.org/

Benno Lossin (1):
  rust: pin-init: internal: remove proc-macro[2] and quote workarounds

Gary Guo (10):
  rust: macros: use `quote!` from vendored crate
  rust: macros: convert `#[vtable]` macro to use `syn`
  rust: macros: use `syn` to parse `module!` macro
  rust: macros: use `quote!` for `module!` macro
  rust: macros: convert `#[export]` to use `syn`
  rust: macros: convert `concat_idents!` to use `syn`
  rust: macros: convert `#[kunit_tests]` macro to use `syn`
  rust: macros: allow arbitrary types to be used in `module!` macro
  rust: macros: rearrange `#[doc(hidden)]` in `module!` macro
  rust: kunit: use `pin_init::zeroed` instead of custom null value

 rust/Makefile                             |  16 +-
 rust/kernel/kunit.rs                      |  26 +-
 rust/macros/concat_idents.rs              |  39 +-
 rust/macros/export.rs                     |  26 +-
 rust/macros/fmt.rs                        |   4 +-
 rust/macros/helpers.rs                    | 129 +---
 rust/macros/kunit.rs                      | 275 +++----
 rust/macros/lib.rs                        |  41 +-
 rust/macros/module.rs                     | 902 ++++++++++++----------
 rust/macros/paste.rs                      |   2 +-
 rust/macros/quote.rs                      | 182 -----
 rust/macros/vtable.rs                     | 164 ++--
 rust/pin-init/internal/src/helpers.rs     |   7 +-
 rust/pin-init/internal/src/lib.rs         |  16 -
 rust/pin-init/internal/src/pin_data.rs    |  18 +-
 rust/pin-init/internal/src/pinned_drop.rs |  10 +-
 rust/pin-init/internal/src/zeroable.rs    |   6 +-
 scripts/generate_rust_analyzer.py         |   2 +-
 18 files changed, 833 insertions(+), 1032 deletions(-)
 delete mode 100644 rust/macros/quote.rs


base-commit: 9ace4753a5202b02191d54e9fdf7f9e3d02b85eb
-- 
2.51.2


^ permalink raw reply	[flat|nested] 23+ messages in thread

* [PATCH v2 01/11] rust: pin-init: internal: remove proc-macro[2] and quote workarounds
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 16:40   ` Tamir Duberstein
  2026-01-07 16:15 ` [PATCH v2 02/11] rust: macros: use `quote!` from vendored crate Gary Guo
                   ` (9 subsequent siblings)
  10 siblings, 1 reply; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Fiona Behrens, Tamir Duberstein
  Cc: rust-for-linux, Greg Kroah-Hartman, Christian Schrefl,
	linux-kernel

From: Benno Lossin <lossin@kernel.org>

The kernel only had the `proc-macro` library available, whereas the
user-space version also used `proc-macro2` and `quote`. Now both are
available to the kernel, making it possible to remove the workarounds.

Clippy complains about unnecessary `.to_string()` as `proc-macro2`
provides additional `PartialEq` impl, so they are removed.

Signed-off-by: Benno Lossin <lossin@kernel.org>
Co-developed-by: Gary Guo <gary@garyguo.net>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/Makefile                             | 16 ++++++++++------
 rust/pin-init/internal/src/helpers.rs     |  7 ++-----
 rust/pin-init/internal/src/lib.rs         | 16 ----------------
 rust/pin-init/internal/src/pin_data.rs    | 18 ++++++------------
 rust/pin-init/internal/src/pinned_drop.rs | 10 ++++------
 rust/pin-init/internal/src/zeroable.rs    |  6 ++----
 scripts/generate_rust_analyzer.py         |  2 +-
 7 files changed, 25 insertions(+), 50 deletions(-)

diff --git a/rust/Makefile b/rust/Makefile
index 5d357dce1704d..96f0ac53ec0e8 100644
--- a/rust/Makefile
+++ b/rust/Makefile
@@ -212,9 +212,10 @@ rustdoc-ffi: $(src)/ffi.rs rustdoc-core FORCE
 
 rustdoc-pin_init_internal: private rustdoc_host = yes
 rustdoc-pin_init_internal: private rustc_target_flags = --cfg kernel \
-    --extern proc_macro --crate-type proc-macro
+    --extern proc_macro --extern proc_macro2 --extern quote --extern syn \
+    --crate-type proc-macro
 rustdoc-pin_init_internal: $(src)/pin-init/internal/src/lib.rs \
-    rustdoc-clean FORCE
+    rustdoc-clean rustdoc-proc_macro2 rustdoc-quote rustdoc-syn FORCE
 	+$(call if_changed,rustdoc)
 
 rustdoc-pin_init: private rustdoc_host = yes
@@ -273,9 +274,10 @@ rusttestlib-macros: $(src)/macros/lib.rs \
 	+$(call if_changed,rustc_test_library)
 
 rusttestlib-pin_init_internal: private rustc_target_flags = --cfg kernel \
-    --extern proc_macro
+    --extern proc_macro --extern proc_macro2 --extern quote --extern syn
 rusttestlib-pin_init_internal: private rustc_test_library_proc = yes
-rusttestlib-pin_init_internal: $(src)/pin-init/internal/src/lib.rs FORCE
+rusttestlib-pin_init_internal: $(src)/pin-init/internal/src/lib.rs \
+    rusttestlib-proc_macro2 rusttestlib-quote rusttestlib-syn FORCE
 	+$(call if_changed,rustc_test_library)
 
 rusttestlib-pin_init: private rustc_target_flags = --extern pin_init_internal \
@@ -547,8 +549,10 @@ $(obj)/$(libmacros_name): $(src)/macros/lib.rs $(obj)/libproc_macro2.rlib \
     $(obj)/libquote.rlib $(obj)/libsyn.rlib FORCE
 	+$(call if_changed_dep,rustc_procmacro)
 
-$(obj)/$(libpin_init_internal_name): private rustc_target_flags = --cfg kernel
-$(obj)/$(libpin_init_internal_name): $(src)/pin-init/internal/src/lib.rs FORCE
+$(obj)/$(libpin_init_internal_name): private rustc_target_flags = --cfg kernel \
+    --extern proc_macro2 --extern quote --extern syn
+$(obj)/$(libpin_init_internal_name): $(src)/pin-init/internal/src/lib.rs \
+	$(obj)/libproc_macro2.rlib $(obj)/libquote.rlib $(obj)/libsyn.rlib FORCE
 	+$(call if_changed_dep,rustc_procmacro)
 
 quiet_cmd_rustc_library = $(if $(skip_clippy),RUSTC,$(RUSTC_OR_CLIPPY_QUIET)) L $@
diff --git a/rust/pin-init/internal/src/helpers.rs b/rust/pin-init/internal/src/helpers.rs
index 236f989a50f2f..90f85eaa41231 100644
--- a/rust/pin-init/internal/src/helpers.rs
+++ b/rust/pin-init/internal/src/helpers.rs
@@ -1,9 +1,6 @@
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
-#[cfg(not(kernel))]
-use proc_macro2 as proc_macro;
-
-use proc_macro::{TokenStream, TokenTree};
+use proc_macro2::{TokenStream, TokenTree};
 
 /// Parsed generics.
 ///
@@ -101,7 +98,7 @@ pub(crate) fn parse_generics(input: TokenStream) -> (Generics, Vec<TokenTree>) {
                     1 => {
                         // Here depending on the token, it might be a generic variable name.
                         match tt.clone() {
-                            TokenTree::Ident(i) if at_start && i.to_string() == "const" => {
+                            TokenTree::Ident(i) if at_start && i == "const" => {
                                 let Some(name) = toks.next() else {
                                     // Parsing error.
                                     break;
diff --git a/rust/pin-init/internal/src/lib.rs b/rust/pin-init/internal/src/lib.rs
index 297b0129a5bfd..4c4dc639ce823 100644
--- a/rust/pin-init/internal/src/lib.rs
+++ b/rust/pin-init/internal/src/lib.rs
@@ -7,27 +7,11 @@
 //! `pin-init` proc macros.
 
 #![cfg_attr(not(RUSTC_LINT_REASONS_IS_STABLE), feature(lint_reasons))]
-// Allow `.into()` to convert
-// - `proc_macro2::TokenStream` into `proc_macro::TokenStream` in the user-space version.
-// - `proc_macro::TokenStream` into `proc_macro::TokenStream` in the kernel version.
-//   Clippy warns on this conversion, but it's required by the user-space version.
-//
-// Remove once we have `proc_macro2` in the kernel.
-#![allow(clippy::useless_conversion)]
 // Documentation is done in the pin-init crate instead.
 #![allow(missing_docs)]
 
 use proc_macro::TokenStream;
 
-#[cfg(kernel)]
-#[path = "../../../macros/quote.rs"]
-#[macro_use]
-#[cfg_attr(not(kernel), rustfmt::skip)]
-mod quote;
-#[cfg(not(kernel))]
-#[macro_use]
-extern crate quote;
-
 mod helpers;
 mod pin_data;
 mod pinned_drop;
diff --git a/rust/pin-init/internal/src/pin_data.rs b/rust/pin-init/internal/src/pin_data.rs
index 87d4a7eb1d35e..86a53b37cc660 100644
--- a/rust/pin-init/internal/src/pin_data.rs
+++ b/rust/pin-init/internal/src/pin_data.rs
@@ -1,10 +1,8 @@
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
-#[cfg(not(kernel))]
-use proc_macro2 as proc_macro;
-
 use crate::helpers::{parse_generics, Generics};
-use proc_macro::{Group, Punct, Spacing, TokenStream, TokenTree};
+use proc_macro2::{Group, Punct, Spacing, TokenStream, TokenTree};
+use quote::quote;
 
 pub(crate) fn pin_data(args: TokenStream, input: TokenStream) -> TokenStream {
     // This proc-macro only does some pre-parsing and then delegates the actual parsing to
@@ -28,7 +26,7 @@ pub(crate) fn pin_data(args: TokenStream, input: TokenStream) -> TokenStream {
     // The name of the struct with ty_generics.
     let struct_name = rest
         .iter()
-        .skip_while(|tt| !matches!(tt, TokenTree::Ident(i) if i.to_string() == "struct"))
+        .skip_while(|tt| !matches!(tt, TokenTree::Ident(i) if i == "struct"))
         .nth(1)
         .and_then(|tt| match tt {
             TokenTree::Ident(_) => {
@@ -65,7 +63,7 @@ pub(crate) fn pin_data(args: TokenStream, input: TokenStream) -> TokenStream {
         .into_iter()
         .flat_map(|tt| {
             // We ignore top level `struct` tokens, since they would emit a compile error.
-            if matches!(&tt, TokenTree::Ident(i) if i.to_string() == "struct") {
+            if matches!(&tt, TokenTree::Ident(i) if i == "struct") {
                 vec![tt]
             } else {
                 replace_self_and_deny_type_defs(&struct_name, tt, &mut errs)
@@ -98,11 +96,7 @@ fn replace_self_and_deny_type_defs(
 ) -> Vec<TokenTree> {
     match tt {
         TokenTree::Ident(ref i)
-            if i.to_string() == "enum"
-                || i.to_string() == "trait"
-                || i.to_string() == "struct"
-                || i.to_string() == "union"
-                || i.to_string() == "impl" =>
+            if i == "enum" || i == "trait" || i == "struct" || i == "union" || i == "impl" =>
         {
             errs.extend(
                 format!(
@@ -119,7 +113,7 @@ fn replace_self_and_deny_type_defs(
             );
             vec![tt]
         }
-        TokenTree::Ident(i) if i.to_string() == "Self" => struct_name.clone(),
+        TokenTree::Ident(i) if i == "Self" => struct_name.clone(),
         TokenTree::Literal(_) | TokenTree::Punct(_) | TokenTree::Ident(_) => vec![tt],
         TokenTree::Group(g) => vec![TokenTree::Group(Group::new(
             g.delimiter(),
diff --git a/rust/pin-init/internal/src/pinned_drop.rs b/rust/pin-init/internal/src/pinned_drop.rs
index c4ca7a70b726a..cf8cd1c429849 100644
--- a/rust/pin-init/internal/src/pinned_drop.rs
+++ b/rust/pin-init/internal/src/pinned_drop.rs
@@ -1,15 +1,13 @@
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
-#[cfg(not(kernel))]
-use proc_macro2 as proc_macro;
-
-use proc_macro::{TokenStream, TokenTree};
+use proc_macro2::{TokenStream, TokenTree};
+use quote::quote;
 
 pub(crate) fn pinned_drop(_args: TokenStream, input: TokenStream) -> TokenStream {
     let mut toks = input.into_iter().collect::<Vec<_>>();
     assert!(!toks.is_empty());
     // Ensure that we have an `impl` item.
-    assert!(matches!(&toks[0], TokenTree::Ident(i) if i.to_string() == "impl"));
+    assert!(matches!(&toks[0], TokenTree::Ident(i) if i == "impl"));
     // Ensure that we are implementing `PinnedDrop`.
     let mut nesting: usize = 0;
     let mut pinned_drop_idx = None;
@@ -27,7 +25,7 @@ pub(crate) fn pinned_drop(_args: TokenStream, input: TokenStream) -> TokenStream
         if i >= 1 && nesting == 0 {
             // Found the end of the generics, this should be `PinnedDrop`.
             assert!(
-                matches!(tt, TokenTree::Ident(i) if i.to_string() == "PinnedDrop"),
+                matches!(tt, TokenTree::Ident(i) if i == "PinnedDrop"),
                 "expected 'PinnedDrop', found: '{tt:?}'"
             );
             pinned_drop_idx = Some(i);
diff --git a/rust/pin-init/internal/src/zeroable.rs b/rust/pin-init/internal/src/zeroable.rs
index e0ed3998445cf..d8a5ef3883f4b 100644
--- a/rust/pin-init/internal/src/zeroable.rs
+++ b/rust/pin-init/internal/src/zeroable.rs
@@ -1,10 +1,8 @@
 // SPDX-License-Identifier: GPL-2.0
 
-#[cfg(not(kernel))]
-use proc_macro2 as proc_macro;
-
 use crate::helpers::{parse_generics, Generics};
-use proc_macro::{TokenStream, TokenTree};
+use proc_macro2::{TokenStream, TokenTree};
+use quote::quote;
 
 pub(crate) fn parse_zeroable_derive_input(
     input: TokenStream,
diff --git a/scripts/generate_rust_analyzer.py b/scripts/generate_rust_analyzer.py
index 147d0cc940681..d31d938886589 100755
--- a/scripts/generate_rust_analyzer.py
+++ b/scripts/generate_rust_analyzer.py
@@ -123,7 +123,7 @@ def generate_crates(srctree, objtree, sysroot_src, external_src, cfgs, core_edit
     append_crate(
         "pin_init_internal",
         srctree / "rust" / "pin-init" / "internal" / "src" / "lib.rs",
-        [],
+        ["std", "proc_macro", "proc_macro2", "quote", "syn"],
         cfg=["kernel"],
         is_proc_macro=True,
     )

base-commit: 9ace4753a5202b02191d54e9fdf7f9e3d02b85eb
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 02/11] rust: macros: use `quote!` from vendored crate
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
  2026-01-07 16:15 ` [PATCH v2 01/11] rust: pin-init: internal: remove proc-macro[2] and quote workarounds Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 16:15 ` [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn` Gary Guo
                   ` (8 subsequent siblings)
  10 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Brendan Higgins, David Gow, Rae Moar,
	Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, Tamir Duberstein, Igor Korotin,
	José Expósito, Greg Kroah-Hartman
  Cc: rust-for-linux, Guilherme Giacomo Simoes, linux-kernel,
	linux-kselftest, kunit-dev, linux-modules

From: Gary Guo <gary@garyguo.net>

With `quote` crate now vendored in the kernel, we can remove our custom
`quote!` macro implementation and just rely on that crate instead.

The `quote` crate uses types from the `proc-macro2` library so we also
update to use that, and perform conversion in the top-level lib.rs.

Clippy complains about unnecessary `.to_string()` as `proc-macro2`
provides additional `PartialEq` impl, so they are removed.

Reviewed-by: Tamir Duberstein <tamird@gmail.com>
Reviewed-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/concat_idents.rs |   2 +-
 rust/macros/export.rs        |   4 +-
 rust/macros/fmt.rs           |   4 +-
 rust/macros/helpers.rs       |   4 +-
 rust/macros/kunit.rs         |   5 +-
 rust/macros/lib.rs           |  21 ++--
 rust/macros/module.rs        |   6 +-
 rust/macros/paste.rs         |   2 +-
 rust/macros/quote.rs         | 182 -----------------------------------
 rust/macros/vtable.rs        |   7 +-
 10 files changed, 32 insertions(+), 205 deletions(-)
 delete mode 100644 rust/macros/quote.rs

diff --git a/rust/macros/concat_idents.rs b/rust/macros/concat_idents.rs
index 7e4b450f3a507..12cb231c3d715 100644
--- a/rust/macros/concat_idents.rs
+++ b/rust/macros/concat_idents.rs
@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use proc_macro::{token_stream, Ident, TokenStream, TokenTree};
+use proc_macro2::{token_stream, Ident, TokenStream, TokenTree};
 
 use crate::helpers::expect_punct;
 
diff --git a/rust/macros/export.rs b/rust/macros/export.rs
index a08f6337d5c8d..92d9b30971929 100644
--- a/rust/macros/export.rs
+++ b/rust/macros/export.rs
@@ -1,7 +1,9 @@
 // SPDX-License-Identifier: GPL-2.0
 
+use proc_macro2::TokenStream;
+use quote::quote;
+
 use crate::helpers::function_name;
-use proc_macro::TokenStream;
 
 /// Please see [`crate::export`] for documentation.
 pub(crate) fn export(_attr: TokenStream, ts: TokenStream) -> TokenStream {
diff --git a/rust/macros/fmt.rs b/rust/macros/fmt.rs
index 2f4b9f6e22110..19f709262552b 100644
--- a/rust/macros/fmt.rs
+++ b/rust/macros/fmt.rs
@@ -1,8 +1,10 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use proc_macro::{Ident, TokenStream, TokenTree};
 use std::collections::BTreeSet;
 
+use proc_macro2::{Ident, TokenStream, TokenTree};
+use quote::quote_spanned;
+
 /// Please see [`crate::fmt`] for documentation.
 pub(crate) fn fmt(input: TokenStream) -> TokenStream {
     let mut input = input.into_iter();
diff --git a/rust/macros/helpers.rs b/rust/macros/helpers.rs
index 365d7eb499c08..13fafaba12261 100644
--- a/rust/macros/helpers.rs
+++ b/rust/macros/helpers.rs
@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use proc_macro::{token_stream, Group, Ident, TokenStream, TokenTree};
+use proc_macro2::{token_stream, Group, Ident, TokenStream, TokenTree};
 
 pub(crate) fn try_ident(it: &mut token_stream::IntoIter) -> Option<String> {
     if let Some(TokenTree::Ident(ident)) = it.next() {
@@ -86,7 +86,7 @@ pub(crate) fn function_name(input: TokenStream) -> Option<Ident> {
     let mut input = input.into_iter();
     while let Some(token) = input.next() {
         match token {
-            TokenTree::Ident(i) if i.to_string() == "fn" => {
+            TokenTree::Ident(i) if i == "fn" => {
                 if let Some(TokenTree::Ident(i)) = input.next() {
                     return Some(i);
                 }
diff --git a/rust/macros/kunit.rs b/rust/macros/kunit.rs
index b395bb0536959..5cd6aa5eef07d 100644
--- a/rust/macros/kunit.rs
+++ b/rust/macros/kunit.rs
@@ -4,10 +4,11 @@
 //!
 //! Copyright (c) 2023 José Expósito <jose.exposito89@gmail.com>
 
-use proc_macro::{Delimiter, Group, TokenStream, TokenTree};
 use std::collections::HashMap;
 use std::fmt::Write;
 
+use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
+
 pub(crate) fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
     let attr = attr.to_string();
 
@@ -59,7 +60,7 @@ pub(crate) fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
                 }
                 _ => (),
             },
-            TokenTree::Ident(i) if i.to_string() == "fn" && attributes.contains_key("test") => {
+            TokenTree::Ident(i) if i == "fn" && attributes.contains_key("test") => {
                 if let Some(TokenTree::Ident(test_name)) = body_it.next() {
                     tests.push((test_name, attributes.remove("cfg").unwrap_or_default()))
                 }
diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
index b38002151871a..945982c21f703 100644
--- a/rust/macros/lib.rs
+++ b/rust/macros/lib.rs
@@ -11,8 +11,6 @@
 // to avoid depending on the full `proc_macro_span` on Rust >= 1.88.0.
 #![cfg_attr(not(CONFIG_RUSTC_HAS_SPAN_FILE), feature(proc_macro_span))]
 
-#[macro_use]
-mod quote;
 mod concat_idents;
 mod export;
 mod fmt;
@@ -132,7 +130,7 @@
 ///     the kernel module.
 #[proc_macro]
 pub fn module(ts: TokenStream) -> TokenStream {
-    module::module(ts)
+    module::module(ts.into()).into()
 }
 
 /// Declares or implements a vtable trait.
@@ -207,7 +205,7 @@ pub fn module(ts: TokenStream) -> TokenStream {
 /// [`kernel::error::VTABLE_DEFAULT_ERROR`]: ../kernel/error/constant.VTABLE_DEFAULT_ERROR.html
 #[proc_macro_attribute]
 pub fn vtable(attr: TokenStream, ts: TokenStream) -> TokenStream {
-    vtable::vtable(attr, ts)
+    vtable::vtable(attr.into(), ts.into()).into()
 }
 
 /// Export a function so that C code can call it via a header file.
@@ -230,7 +228,7 @@ pub fn vtable(attr: TokenStream, ts: TokenStream) -> TokenStream {
 /// automatically exported with `EXPORT_SYMBOL_GPL`.
 #[proc_macro_attribute]
 pub fn export(attr: TokenStream, ts: TokenStream) -> TokenStream {
-    export::export(attr, ts)
+    export::export(attr.into(), ts.into()).into()
 }
 
 /// Like [`core::format_args!`], but automatically wraps arguments in [`kernel::fmt::Adapter`].
@@ -248,7 +246,7 @@ pub fn export(attr: TokenStream, ts: TokenStream) -> TokenStream {
 /// [`pr_info!`]: ../kernel/macro.pr_info.html
 #[proc_macro]
 pub fn fmt(input: TokenStream) -> TokenStream {
-    fmt::fmt(input)
+    fmt::fmt(input.into()).into()
 }
 
 /// Concatenate two identifiers.
@@ -306,7 +304,7 @@ pub fn fmt(input: TokenStream) -> TokenStream {
 /// ```
 #[proc_macro]
 pub fn concat_idents(ts: TokenStream) -> TokenStream {
-    concat_idents::concat_idents(ts)
+    concat_idents::concat_idents(ts.into()).into()
 }
 
 /// Paste identifiers together.
@@ -444,9 +442,12 @@ pub fn concat_idents(ts: TokenStream) -> TokenStream {
 /// [`paste`]: https://docs.rs/paste/
 #[proc_macro]
 pub fn paste(input: TokenStream) -> TokenStream {
-    let mut tokens = input.into_iter().collect();
+    let mut tokens = proc_macro2::TokenStream::from(input).into_iter().collect();
     paste::expand(&mut tokens);
-    tokens.into_iter().collect()
+    tokens
+        .into_iter()
+        .collect::<proc_macro2::TokenStream>()
+        .into()
 }
 
 /// Registers a KUnit test suite and its test cases using a user-space like syntax.
@@ -473,5 +474,5 @@ pub fn paste(input: TokenStream) -> TokenStream {
 /// ```
 #[proc_macro_attribute]
 pub fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
-    kunit::kunit_tests(attr, ts)
+    kunit::kunit_tests(attr.into(), ts.into()).into()
 }
diff --git a/rust/macros/module.rs b/rust/macros/module.rs
index 80cb9b16f5aaf..b855a2b586e18 100644
--- a/rust/macros/module.rs
+++ b/rust/macros/module.rs
@@ -1,9 +1,11 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use crate::helpers::*;
-use proc_macro::{token_stream, Delimiter, Literal, TokenStream, TokenTree};
 use std::fmt::Write;
 
+use proc_macro2::{token_stream, Delimiter, Literal, TokenStream, TokenTree};
+
+use crate::helpers::*;
+
 fn expect_string_array(it: &mut token_stream::IntoIter) -> Vec<String> {
     let group = expect_group(it);
     assert_eq!(group.delimiter(), Delimiter::Bracket);
diff --git a/rust/macros/paste.rs b/rust/macros/paste.rs
index cce712d19855b..2181e312a7d32 100644
--- a/rust/macros/paste.rs
+++ b/rust/macros/paste.rs
@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use proc_macro::{Delimiter, Group, Ident, Spacing, Span, TokenTree};
+use proc_macro2::{Delimiter, Group, Ident, Spacing, Span, TokenTree};
 
 fn concat_helper(tokens: &[TokenTree]) -> Vec<(String, Span)> {
     let mut tokens = tokens.iter();
diff --git a/rust/macros/quote.rs b/rust/macros/quote.rs
deleted file mode 100644
index ddfc21577539c..0000000000000
--- a/rust/macros/quote.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0 OR MIT
-
-use proc_macro::{TokenStream, TokenTree};
-
-pub(crate) trait ToTokens {
-    fn to_tokens(&self, tokens: &mut TokenStream);
-}
-
-impl<T: ToTokens> ToTokens for Option<T> {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        if let Some(v) = self {
-            v.to_tokens(tokens);
-        }
-    }
-}
-
-impl ToTokens for proc_macro::Group {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        tokens.extend([TokenTree::from(self.clone())]);
-    }
-}
-
-impl ToTokens for proc_macro::Ident {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        tokens.extend([TokenTree::from(self.clone())]);
-    }
-}
-
-impl ToTokens for TokenTree {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        tokens.extend([self.clone()]);
-    }
-}
-
-impl ToTokens for TokenStream {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        tokens.extend(self.clone());
-    }
-}
-
-/// Converts tokens into [`proc_macro::TokenStream`] and performs variable interpolations with
-/// the given span.
-///
-/// This is a similar to the
-/// [`quote_spanned!`](https://docs.rs/quote/latest/quote/macro.quote_spanned.html) macro from the
-/// `quote` crate but provides only just enough functionality needed by the current `macros` crate.
-macro_rules! quote_spanned {
-    ($span:expr => $($tt:tt)*) => {{
-        let mut tokens = ::proc_macro::TokenStream::new();
-        {
-            #[allow(unused_variables)]
-            let span = $span;
-            quote_spanned!(@proc tokens span $($tt)*);
-        }
-        tokens
-    }};
-    (@proc $v:ident $span:ident) => {};
-    (@proc $v:ident $span:ident #$id:ident $($tt:tt)*) => {
-        $crate::quote::ToTokens::to_tokens(&$id, &mut $v);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident #(#$id:ident)* $($tt:tt)*) => {
-        for token in $id {
-            $crate::quote::ToTokens::to_tokens(&token, &mut $v);
-        }
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident ( $($inner:tt)* ) $($tt:tt)*) => {
-        #[allow(unused_mut)]
-        let mut tokens = ::proc_macro::TokenStream::new();
-        quote_spanned!(@proc tokens $span $($inner)*);
-        $v.extend([::proc_macro::TokenTree::Group(::proc_macro::Group::new(
-            ::proc_macro::Delimiter::Parenthesis,
-            tokens,
-        ))]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident [ $($inner:tt)* ] $($tt:tt)*) => {
-        let mut tokens = ::proc_macro::TokenStream::new();
-        quote_spanned!(@proc tokens $span $($inner)*);
-        $v.extend([::proc_macro::TokenTree::Group(::proc_macro::Group::new(
-            ::proc_macro::Delimiter::Bracket,
-            tokens,
-        ))]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident { $($inner:tt)* } $($tt:tt)*) => {
-        let mut tokens = ::proc_macro::TokenStream::new();
-        quote_spanned!(@proc tokens $span $($inner)*);
-        $v.extend([::proc_macro::TokenTree::Group(::proc_macro::Group::new(
-            ::proc_macro::Delimiter::Brace,
-            tokens,
-        ))]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident :: $($tt:tt)*) => {
-        $v.extend([::proc_macro::Spacing::Joint, ::proc_macro::Spacing::Alone].map(|spacing| {
-            ::proc_macro::TokenTree::Punct(::proc_macro::Punct::new(':', spacing))
-        }));
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident : $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new(':', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident , $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new(',', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident @ $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new('@', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident ! $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new('!', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident ; $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new(';', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident + $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new('+', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident = $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new('=', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident # $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new('#', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident & $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Punct(
-            ::proc_macro::Punct::new('&', ::proc_macro::Spacing::Alone),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident _ $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Ident(
-            ::proc_macro::Ident::new("_", $span),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-    (@proc $v:ident $span:ident $id:ident $($tt:tt)*) => {
-        $v.extend([::proc_macro::TokenTree::Ident(
-            ::proc_macro::Ident::new(stringify!($id), $span),
-        )]);
-        quote_spanned!(@proc $v $span $($tt)*);
-    };
-}
-
-/// Converts tokens into [`proc_macro::TokenStream`] and performs variable interpolations with
-/// mixed site span ([`Span::mixed_site()`]).
-///
-/// This is a similar to the [`quote!`](https://docs.rs/quote/latest/quote/macro.quote.html) macro
-/// from the `quote` crate but provides only just enough functionality needed by the current
-/// `macros` crate.
-///
-/// [`Span::mixed_site()`]: https://doc.rust-lang.org/proc_macro/struct.Span.html#method.mixed_site
-macro_rules! quote {
-    ($($tt:tt)*) => {
-        quote_spanned!(::proc_macro::Span::mixed_site() => $($tt)*)
-    }
-}
diff --git a/rust/macros/vtable.rs b/rust/macros/vtable.rs
index ee06044fcd4f3..a67d1cc81a2d3 100644
--- a/rust/macros/vtable.rs
+++ b/rust/macros/vtable.rs
@@ -1,9 +1,10 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use proc_macro::{Delimiter, Group, TokenStream, TokenTree};
 use std::collections::HashSet;
 use std::fmt::Write;
 
+use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
+
 pub(crate) fn vtable(_attr: TokenStream, ts: TokenStream) -> TokenStream {
     let mut tokens: Vec<_> = ts.into_iter().collect();
 
@@ -31,7 +32,7 @@ pub(crate) fn vtable(_attr: TokenStream, ts: TokenStream) -> TokenStream {
     let mut consts = HashSet::new();
     while let Some(token) = body_it.next() {
         match token {
-            TokenTree::Ident(ident) if ident.to_string() == "fn" => {
+            TokenTree::Ident(ident) if ident == "fn" => {
                 let fn_name = match body_it.next() {
                     Some(TokenTree::Ident(ident)) => ident.to_string(),
                     // Possibly we've encountered a fn pointer type instead.
@@ -39,7 +40,7 @@ pub(crate) fn vtable(_attr: TokenStream, ts: TokenStream) -> TokenStream {
                 };
                 functions.push(fn_name);
             }
-            TokenTree::Ident(ident) if ident.to_string() == "const" => {
+            TokenTree::Ident(ident) if ident == "const" => {
                 let const_name = match body_it.next() {
                     Some(TokenTree::Ident(ident)) => ident.to_string(),
                     // Possibly we've encountered an inline const block instead.
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn`
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
  2026-01-07 16:15 ` [PATCH v2 01/11] rust: pin-init: internal: remove proc-macro[2] and quote workarounds Gary Guo
  2026-01-07 16:15 ` [PATCH v2 02/11] rust: macros: use `quote!` from vendored crate Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 16:48   ` Tamir Duberstein
  2026-01-11 17:03   ` Benno Lossin
  2026-01-07 16:15 ` [PATCH v2 04/11] rust: macros: use `syn` to parse `module!` macro Gary Guo
                   ` (7 subsequent siblings)
  10 siblings, 2 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Tamir Duberstein, Igor Korotin,
	José Expósito
  Cc: rust-for-linux, linux-kernel

From: Gary Guo <gary@garyguo.net>

`#[vtable]` is converted to use syn. This is more robust than the
previous heuristic-based searching of defined methods and functions.

When doing so, the trait and impl are split into two code paths as the
types are distinct when parsed by `syn`.

Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/lib.rs    |   9 ++-
 rust/macros/vtable.rs | 163 ++++++++++++++++++++++--------------------
 2 files changed, 93 insertions(+), 79 deletions(-)

diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
index 945982c21f703..9955c04dbaae3 100644
--- a/rust/macros/lib.rs
+++ b/rust/macros/lib.rs
@@ -22,6 +22,8 @@
 
 use proc_macro::TokenStream;
 
+use syn::parse_macro_input;
+
 /// Declares a kernel module.
 ///
 /// The `type` argument should be a type which implements the [`Module`]
@@ -204,8 +206,11 @@ pub fn module(ts: TokenStream) -> TokenStream {
 ///
 /// [`kernel::error::VTABLE_DEFAULT_ERROR`]: ../kernel/error/constant.VTABLE_DEFAULT_ERROR.html
 #[proc_macro_attribute]
-pub fn vtable(attr: TokenStream, ts: TokenStream) -> TokenStream {
-    vtable::vtable(attr.into(), ts.into()).into()
+pub fn vtable(attr: TokenStream, input: TokenStream) -> TokenStream {
+    parse_macro_input!(attr as syn::parse::Nothing);
+    vtable::vtable(parse_macro_input!(input))
+        .unwrap_or_else(|e| e.into_compile_error())
+        .into()
 }
 
 /// Export a function so that C code can call it via a header file.
diff --git a/rust/macros/vtable.rs b/rust/macros/vtable.rs
index a67d1cc81a2d3..a39bedb703973 100644
--- a/rust/macros/vtable.rs
+++ b/rust/macros/vtable.rs
@@ -1,97 +1,106 @@
 // SPDX-License-Identifier: GPL-2.0
 
 use std::collections::HashSet;
-use std::fmt::Write;
 
-use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
+use proc_macro2::{
+    Ident,
+    TokenStream, //
+};
+use quote::ToTokens;
+use syn::{
+    parse_quote,
+    Error,
+    ImplItem,
+    Item,
+    ItemImpl,
+    ItemTrait,
+    Result,
+    TraitItem, //
+};
 
-pub(crate) fn vtable(_attr: TokenStream, ts: TokenStream) -> TokenStream {
-    let mut tokens: Vec<_> = ts.into_iter().collect();
+fn handle_trait(mut item: ItemTrait) -> Result<ItemTrait> {
+    let mut functions = Vec::new();
+    for item in &item.items {
+        if let TraitItem::Fn(fn_item) = item {
+            functions.push(fn_item.sig.ident.clone());
+        }
+    }
+
+    item.items.push(parse_quote! {
+         /// A marker to prevent implementors from forgetting to use [`#[vtable]`](vtable)
+         /// attribute when implementing this trait.
+         const USE_VTABLE_ATTR: ();
+    });
 
-    // Scan for the `trait` or `impl` keyword.
-    let is_trait = tokens
-        .iter()
-        .find_map(|token| match token {
-            TokenTree::Ident(ident) => match ident.to_string().as_str() {
-                "trait" => Some(true),
-                "impl" => Some(false),
-                _ => None,
-            },
-            _ => None,
-        })
-        .expect("#[vtable] attribute should only be applied to trait or impl block");
+    let mut consts = HashSet::new();
+    for name in functions {
+        let gen_const_name = Ident::new(
+            &format!("HAS_{}", name.to_string().to_uppercase()),
+            name.span(),
+        );
+        // Skip if it's declared already -- this can happen if `#[cfg]` is used to selectively
+        // define functions.
+        // FIXME: `#[cfg]` should be copied and propagated to the generated consts.
+        if consts.contains(&gen_const_name) {
+            continue;
+        }
+        // We don't know on the implementation-site whether a method is required or provided
+        // so we have to generate a const for all methods.
+        let comment = format!("Indicates if the `{name}` method is overridden by the implementor.");
+        item.items.push(parse_quote! {
+            #[doc = #comment]
+            const #gen_const_name: bool = false;
+        });
+        consts.insert(gen_const_name);
+    }
 
-    // Retrieve the main body. The main body should be the last token tree.
-    let body = match tokens.pop() {
-        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => group,
-        _ => panic!("cannot locate main body of trait or impl block"),
-    };
+    Ok(item)
+}
 
-    let mut body_it = body.stream().into_iter();
+fn handle_impl(mut item: ItemImpl) -> Result<ItemImpl> {
     let mut functions = Vec::new();
     let mut consts = HashSet::new();
-    while let Some(token) = body_it.next() {
-        match token {
-            TokenTree::Ident(ident) if ident == "fn" => {
-                let fn_name = match body_it.next() {
-                    Some(TokenTree::Ident(ident)) => ident.to_string(),
-                    // Possibly we've encountered a fn pointer type instead.
-                    _ => continue,
-                };
-                functions.push(fn_name);
+    for item in &item.items {
+        match item {
+            ImplItem::Fn(fn_item) => {
+                functions.push(fn_item.sig.ident.clone());
             }
-            TokenTree::Ident(ident) if ident == "const" => {
-                let const_name = match body_it.next() {
-                    Some(TokenTree::Ident(ident)) => ident.to_string(),
-                    // Possibly we've encountered an inline const block instead.
-                    _ => continue,
-                };
-                consts.insert(const_name);
+            ImplItem::Const(const_item) => {
+                consts.insert(const_item.ident.clone());
             }
-            _ => (),
+            _ => {}
         }
     }
 
-    let mut const_items;
-    if is_trait {
-        const_items = "
-                /// A marker to prevent implementors from forgetting to use [`#[vtable]`](vtable)
-                /// attribute when implementing this trait.
-                const USE_VTABLE_ATTR: ();
-        "
-        .to_owned();
+    item.items.push(parse_quote! {
+         const USE_VTABLE_ATTR: () = ();
+    });
 
-        for f in functions {
-            let gen_const_name = format!("HAS_{}", f.to_uppercase());
-            // Skip if it's declared already -- this allows user override.
-            if consts.contains(&gen_const_name) {
-                continue;
-            }
-            // We don't know on the implementation-site whether a method is required or provided
-            // so we have to generate a const for all methods.
-            write!(
-                const_items,
-                "/// Indicates if the `{f}` method is overridden by the implementor.
-                const {gen_const_name}: bool = false;",
-            )
-            .unwrap();
-            consts.insert(gen_const_name);
-        }
-    } else {
-        const_items = "const USE_VTABLE_ATTR: () = ();".to_owned();
-
-        for f in functions {
-            let gen_const_name = format!("HAS_{}", f.to_uppercase());
-            if consts.contains(&gen_const_name) {
-                continue;
-            }
-            write!(const_items, "const {gen_const_name}: bool = true;").unwrap();
+    for name in functions {
+        let gen_const_name = Ident::new(
+            &format!("HAS_{}", name.to_string().to_uppercase()),
+            name.span(),
+        );
+        // Skip if it's declared already -- this allows user override.
+        if consts.contains(&gen_const_name) {
+            continue;
         }
+        item.items.push(parse_quote! {
+            const #gen_const_name: bool = true;
+        });
+        consts.insert(gen_const_name);
     }
 
-    let new_body = vec![const_items.parse().unwrap(), body.stream()]
-        .into_iter()
-        .collect();
-    tokens.push(TokenTree::Group(Group::new(Delimiter::Brace, new_body)));
-    tokens.into_iter().collect()
+    Ok(item)
+}
+
+pub(crate) fn vtable(input: Item) -> Result<TokenStream> {
+    match input {
+        Item::Trait(item) => Ok(handle_trait(item)?.into_token_stream()),
+        Item::Impl(item) => Ok(handle_impl(item)?.into_token_stream()),
+        _ => Err(Error::new_spanned(
+            input,
+            "`#[vtable]` attribute should only be applied to trait or impl block",
+        ))?,
+    }
 }
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 04/11] rust: macros: use `syn` to parse `module!` macro
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
                   ` (2 preceding siblings ...)
  2026-01-07 16:15 ` [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn` Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 17:11   ` Tamir Duberstein
  2026-01-07 16:15 ` [PATCH v2 05/11] rust: macros: use `quote!` for " Gary Guo
                   ` (6 subsequent siblings)
  10 siblings, 1 reply; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Luis Chamberlain, Petr Pavlu, Daniel Gomez,
	Sami Tolvanen, Aaron Tomlin, Tamir Duberstein,
	José Expósito
  Cc: rust-for-linux, Patrick Miller, linux-kernel, linux-modules

From: Gary Guo <gary@garyguo.net>

With `syn` being available in the kernel, use it to parse the complex
custom `module!` macro to replace existing helpers. Only parsing is
changed in this commit, the code generation is untouched.

This has the benefit of better error message when the macro is used
incorrectly, as it can point to a concrete span on what's going wrong.

For example, if a field is specified twice, previously it reads:

    error: proc macro panicked
      --> samples/rust/rust_minimal.rs:7:1
       |
    7  | / module! {
    8  | |     type: RustMinimal,
    9  | |     name: "rust_minimal",
    10 | |     author: "Rust for Linux Contributors",
    11 | |     description: "Rust minimal sample",
    12 | |     license: "GPL",
    13 | |     license: "GPL",
    14 | | }
       | |_^
       |
       = help: message: Duplicated key "license". Keys can only be specified once.

now it reads:

    error: duplicated key "license". Keys can only be specified once.
      --> samples/rust/rust_minimal.rs:13:5
       |
    13 |     license: "GPL",
       |     ^^^^^^^

Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/helpers.rs | 109 ++++--------
 rust/macros/lib.rs     |   6 +-
 rust/macros/module.rs  | 389 +++++++++++++++++++++++++----------------
 3 files changed, 277 insertions(+), 227 deletions(-)

diff --git a/rust/macros/helpers.rs b/rust/macros/helpers.rs
index 13fafaba12261..fa66ef6eb0f3d 100644
--- a/rust/macros/helpers.rs
+++ b/rust/macros/helpers.rs
@@ -1,53 +1,21 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use proc_macro2::{token_stream, Group, Ident, TokenStream, TokenTree};
-
-pub(crate) fn try_ident(it: &mut token_stream::IntoIter) -> Option<String> {
-    if let Some(TokenTree::Ident(ident)) = it.next() {
-        Some(ident.to_string())
-    } else {
-        None
-    }
-}
-
-pub(crate) fn try_sign(it: &mut token_stream::IntoIter) -> Option<char> {
-    let peek = it.clone().next();
-    match peek {
-        Some(TokenTree::Punct(punct)) if punct.as_char() == '-' => {
-            let _ = it.next();
-            Some(punct.as_char())
-        }
-        _ => None,
-    }
-}
-
-pub(crate) fn try_literal(it: &mut token_stream::IntoIter) -> Option<String> {
-    if let Some(TokenTree::Literal(literal)) = it.next() {
-        Some(literal.to_string())
-    } else {
-        None
-    }
-}
-
-pub(crate) fn try_string(it: &mut token_stream::IntoIter) -> Option<String> {
-    try_literal(it).and_then(|string| {
-        if string.starts_with('\"') && string.ends_with('\"') {
-            let content = &string[1..string.len() - 1];
-            if content.contains('\\') {
-                panic!("Escape sequences in string literals not yet handled");
-            }
-            Some(content.to_string())
-        } else if string.starts_with("r\"") {
-            panic!("Raw string literals are not yet handled");
-        } else {
-            None
-        }
-    })
-}
-
-pub(crate) fn expect_ident(it: &mut token_stream::IntoIter) -> String {
-    try_ident(it).expect("Expected Ident")
-}
+use proc_macro2::{
+    token_stream,
+    Ident,
+    TokenStream,
+    TokenTree, //
+};
+use quote::ToTokens;
+use syn::{
+    parse::{
+        Parse,
+        ParseStream, //
+    },
+    Error,
+    LitStr,
+    Result, //
+};
 
 pub(crate) fn expect_punct(it: &mut token_stream::IntoIter) -> char {
     if let TokenTree::Punct(punct) = it.next().expect("Reached end of token stream for Punct") {
@@ -57,27 +25,28 @@ pub(crate) fn expect_punct(it: &mut token_stream::IntoIter) -> char {
     }
 }
 
-pub(crate) fn expect_string(it: &mut token_stream::IntoIter) -> String {
-    try_string(it).expect("Expected string")
-}
+/// A string literal that is required to have ASCII value only.
+pub(crate) struct AsciiLitStr(LitStr);
 
-pub(crate) fn expect_string_ascii(it: &mut token_stream::IntoIter) -> String {
-    let string = try_string(it).expect("Expected string");
-    assert!(string.is_ascii(), "Expected ASCII string");
-    string
+impl Parse for AsciiLitStr {
+    fn parse(input: ParseStream<'_>) -> Result<Self> {
+        let s: LitStr = input.parse()?;
+        if !s.value().is_ascii() {
+            return Err(Error::new_spanned(s, "expected ASCII-only string literal"));
+        }
+        Ok(Self(s))
+    }
 }
 
-pub(crate) fn expect_group(it: &mut token_stream::IntoIter) -> Group {
-    if let TokenTree::Group(group) = it.next().expect("Reached end of token stream for Group") {
-        group
-    } else {
-        panic!("Expected Group");
+impl ToTokens for AsciiLitStr {
+    fn to_tokens(&self, ts: &mut TokenStream) {
+        self.0.to_tokens(ts);
     }
 }
 
-pub(crate) fn expect_end(it: &mut token_stream::IntoIter) {
-    if it.next().is_some() {
-        panic!("Expected end");
+impl AsciiLitStr {
+    pub(crate) fn value(&self) -> String {
+        self.0.value()
     }
 }
 
@@ -114,17 +83,3 @@ pub(crate) fn file() -> String {
         proc_macro::Span::call_site().file()
     }
 }
-
-/// Parse a token stream of the form `expected_name: "value",` and return the
-/// string in the position of "value".
-///
-/// # Panics
-///
-/// - On parse error.
-pub(crate) fn expect_string_field(it: &mut token_stream::IntoIter, expected_name: &str) -> String {
-    assert_eq!(expect_ident(it), expected_name);
-    assert_eq!(expect_punct(it), ':');
-    let string = expect_string(it);
-    assert_eq!(expect_punct(it), ',');
-    string
-}
diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
index 9955c04dbaae3..c5347127a3a51 100644
--- a/rust/macros/lib.rs
+++ b/rust/macros/lib.rs
@@ -131,8 +131,10 @@
 ///   - `firmware`: array of ASCII string literals of the firmware files of
 ///     the kernel module.
 #[proc_macro]
-pub fn module(ts: TokenStream) -> TokenStream {
-    module::module(ts.into()).into()
+pub fn module(input: TokenStream) -> TokenStream {
+    module::module(parse_macro_input!(input))
+        .unwrap_or_else(|e| e.into_compile_error())
+        .into()
 }
 
 /// Declares or implements a vtable trait.
diff --git a/rust/macros/module.rs b/rust/macros/module.rs
index b855a2b586e18..6ad7b411ccde4 100644
--- a/rust/macros/module.rs
+++ b/rust/macros/module.rs
@@ -2,28 +2,30 @@
 
 use std::fmt::Write;
 
-use proc_macro2::{token_stream, Delimiter, Literal, TokenStream, TokenTree};
+use proc_macro2::{
+    Literal,
+    TokenStream, //
+};
+use quote::ToTokens;
+use syn::{
+    braced,
+    bracketed,
+    ext::IdentExt,
+    parse::{
+        Parse,
+        ParseStream, //
+    },
+    punctuated::Punctuated,
+    Error,
+    Expr,
+    Ident,
+    LitStr,
+    Result,
+    Token, //
+};
 
 use crate::helpers::*;
 
-fn expect_string_array(it: &mut token_stream::IntoIter) -> Vec<String> {
-    let group = expect_group(it);
-    assert_eq!(group.delimiter(), Delimiter::Bracket);
-    let mut values = Vec::new();
-    let mut it = group.stream().into_iter();
-
-    while let Some(val) = try_string(&mut it) {
-        assert!(val.is_ascii(), "Expected ASCII string");
-        values.push(val);
-        match it.next() {
-            Some(TokenTree::Punct(punct)) => assert_eq!(punct.as_char(), ','),
-            None => break,
-            _ => panic!("Expected ',' or end of array"),
-        }
-    }
-    values
-}
-
 struct ModInfoBuilder<'a> {
     module: &'a str,
     counter: usize,
@@ -113,12 +115,16 @@ fn emit_params(&mut self, info: &ModuleInfo) {
         };
 
         for param in params {
-            let ops = param_ops_path(&param.ptype);
+            let param_name = param.name.to_string();
+            let param_type = param.ptype.to_string();
+            let param_default = param.default.to_token_stream().to_string();
+
+            let ops = param_ops_path(&param_type);
 
             // Note: The spelling of these fields is dictated by the user space
             // tool `modinfo`.
-            self.emit_param("parmtype", &param.name, &param.ptype);
-            self.emit_param("parm", &param.name, &param.description);
+            self.emit_param("parmtype", &param_name, &param_type);
+            self.emit_param("parm", &param_name, &param.description.value());
 
             write!(
                 self.param_buffer,
@@ -160,10 +166,7 @@ fn emit_params(&mut self, info: &ModuleInfo) {
                         );
                 }};
                 ",
-                module_name = info.name,
-                param_type = param.ptype,
-                param_default = param.default,
-                param_name = param.name,
+                module_name = info.name.value(),
                 ops = ops,
             )
             .unwrap();
@@ -187,127 +190,82 @@ fn param_ops_path(param_type: &str) -> &'static str {
     }
 }
 
-fn expect_param_default(param_it: &mut token_stream::IntoIter) -> String {
-    assert_eq!(expect_ident(param_it), "default");
-    assert_eq!(expect_punct(param_it), ':');
-    let sign = try_sign(param_it);
-    let default = try_literal(param_it).expect("Expected default param value");
-    assert_eq!(expect_punct(param_it), ',');
-    let mut value = sign.map(String::from).unwrap_or_default();
-    value.push_str(&default);
-    value
-}
-
-#[derive(Debug, Default)]
-struct ModuleInfo {
-    type_: String,
-    license: String,
-    name: String,
-    authors: Option<Vec<String>>,
-    description: Option<String>,
-    alias: Option<Vec<String>>,
-    firmware: Option<Vec<String>>,
-    imports_ns: Option<Vec<String>>,
-    params: Option<Vec<Parameter>>,
-}
-
-#[derive(Debug)]
-struct Parameter {
-    name: String,
-    ptype: String,
-    default: String,
-    description: String,
-}
-
-fn expect_params(it: &mut token_stream::IntoIter) -> Vec<Parameter> {
-    let params = expect_group(it);
-    assert_eq!(params.delimiter(), Delimiter::Brace);
-    let mut it = params.stream().into_iter();
-    let mut parsed = Vec::new();
-
-    loop {
-        let param_name = match it.next() {
-            Some(TokenTree::Ident(ident)) => ident.to_string(),
-            Some(_) => panic!("Expected Ident or end"),
-            None => break,
-        };
-
-        assert_eq!(expect_punct(&mut it), ':');
-        let param_type = expect_ident(&mut it);
-        let group = expect_group(&mut it);
-        assert_eq!(group.delimiter(), Delimiter::Brace);
-        assert_eq!(expect_punct(&mut it), ',');
-
-        let mut param_it = group.stream().into_iter();
-        let param_default = expect_param_default(&mut param_it);
-        let param_description = expect_string_field(&mut param_it, "description");
-        expect_end(&mut param_it);
-
-        parsed.push(Parameter {
-            name: param_name,
-            ptype: param_type,
-            default: param_default,
-            description: param_description,
-        })
-    }
-
-    parsed
-}
-
-impl ModuleInfo {
-    fn parse(it: &mut token_stream::IntoIter) -> Self {
-        let mut info = ModuleInfo::default();
-
-        const EXPECTED_KEYS: &[&str] = &[
-            "type",
-            "name",
-            "authors",
-            "description",
-            "license",
-            "alias",
-            "firmware",
-            "imports_ns",
-            "params",
-        ];
-        const REQUIRED_KEYS: &[&str] = &["type", "name", "license"];
+/// Parse fields that are required to use a specific order.
+///
+/// As fields must follow a specific order, we *could* just parse fields one by one by peeking.
+/// However the error message generated when implementing that way is not very friendly.
+///
+/// So instead we parse fields in an arbitrary order, but only enforce the ordering after parsing,
+/// and if the wrong order is used, the proper order is communicated to the user with error message.
+///
+/// Usage looks like this:
+/// ```ignore
+/// parse_ordered_fields! {
+///     from input;
+///
+///     // This will extract "foo: <field>" into a variable named "foo".
+///     // The variable will have type `Option<_>`.
+///     foo => <expression that parses the field>,
+///
+///     // If you need the variable name to be different than the key name.
+///     // This extracts "baz: <field>" into a variable named "bar".
+///     // You might want this if "baz" is a keyword.
+///     baz as bar => <expression that parse the field>,
+///
+///     // You can mark a key as required, and the variable will no longer be `Option`.
+///     // foobar will be of type `Expr` instead of `Option<Expr>`.
+///     foobar [required] => input.parse::<Expr>()?,
+/// }
+/// ```
+macro_rules! parse_ordered_fields {
+    (@gen
+        [$input:expr]
+        [$([$name:ident; $key:ident; $parser:expr])*]
+        [$([$req_name:ident; $req_key:ident])*]
+    ) => {
+        $(let mut $name = None;)*
+
+        const EXPECTED_KEYS: &[&str] = &[$(stringify!($key),)*];
+        const REQUIRED_KEYS: &[&str] = &[$(stringify!($req_key),)*];
+
+        let span = $input.span();
         let mut seen_keys = Vec::new();
 
         loop {
-            let key = match it.next() {
-                Some(TokenTree::Ident(ident)) => ident.to_string(),
-                Some(_) => panic!("Expected Ident or end"),
-                None => break,
-            };
+            if $input.is_empty() {
+                break;
+            }
+
+            let key = $input.call(Ident::parse_any)?;
 
             if seen_keys.contains(&key) {
-                panic!("Duplicated key \"{key}\". Keys can only be specified once.");
+                Err(Error::new_spanned(
+                    &key,
+                    format!(r#"duplicated key "{key}". Keys can only be specified once."#),
+                ))?
             }
 
-            assert_eq!(expect_punct(it), ':');
-
-            match key.as_str() {
-                "type" => info.type_ = expect_ident(it),
-                "name" => info.name = expect_string_ascii(it),
-                "authors" => info.authors = Some(expect_string_array(it)),
-                "description" => info.description = Some(expect_string(it)),
-                "license" => info.license = expect_string_ascii(it),
-                "alias" => info.alias = Some(expect_string_array(it)),
-                "firmware" => info.firmware = Some(expect_string_array(it)),
-                "imports_ns" => info.imports_ns = Some(expect_string_array(it)),
-                "params" => info.params = Some(expect_params(it)),
-                _ => panic!("Unknown key \"{key}\". Valid keys are: {EXPECTED_KEYS:?}."),
+            $input.parse::<Token![:]>()?;
+
+            match &*key.to_string() {
+                $(
+                    stringify!($key) => $name = Some($parser),
+                )*
+                _ => {
+                    Err(Error::new_spanned(
+                        &key,
+                        format!(r#"unknown key "{key}". Valid keys are: {EXPECTED_KEYS:?}."#),
+                    ))?
+                }
             }
 
-            assert_eq!(expect_punct(it), ',');
-
+            $input.parse::<Token![,]>()?;
             seen_keys.push(key);
         }
 
-        expect_end(it);
-
         for key in REQUIRED_KEYS {
             if !seen_keys.iter().any(|e| e == key) {
-                panic!("Missing required key \"{key}\".");
+                Err(Error::new(span, format!(r#"missing required key "{key}""#)))?
             }
         }
 
@@ -319,43 +277,178 @@ fn parse(it: &mut token_stream::IntoIter) -> Self {
         }
 
         if seen_keys != ordered_keys {
-            panic!("Keys are not ordered as expected. Order them like: {ordered_keys:?}.");
+            Err(Error::new(
+                span,
+                format!(r#"keys are not ordered as expected. Order them like: {ordered_keys:?}."#),
+            ))?
+        }
+
+        $(let $req_name = $req_name.expect("required field");)*
+    };
+
+    // Handle required fields.
+    (@gen
+        [$input:expr] [$($tok:tt)*] [$($req:tt)*]
+        $key:ident as $name:ident [required] => $parser:expr,
+        $($rest:tt)*
+    ) => {
+        parse_ordered_fields!(
+            @gen [$input] [$($tok)* [$name; $key; $parser]] [$($req)* [$name; $key]] $($rest)*
+        )
+    };
+    (@gen
+        [$input:expr] [$($tok:tt)*] [$($req:tt)*]
+        $name:ident [required] => $parser:expr,
+        $($rest:tt)*
+    ) => {
+        parse_ordered_fields!(
+            @gen [$input] [$($tok)* [$name; $name; $parser]] [$($req)* [$name; $name]] $($rest)*
+        )
+    };
+
+    // Handle optional fields.
+    (@gen
+        [$input:expr] [$($tok:tt)*] [$($req:tt)*]
+        $key:ident as $name:ident => $parser:expr,
+        $($rest:tt)*
+    ) => {
+        parse_ordered_fields!(
+            @gen [$input] [$($tok)* [$name; $key; $parser]] [$($req)*] $($rest)*
+        )
+    };
+    (@gen
+        [$input:expr] [$($tok:tt)*] [$($req:tt)*]
+        $name:ident => $parser:expr,
+        $($rest:tt)*
+    ) => {
+        parse_ordered_fields!(
+            @gen [$input] [$($tok)* [$name; $name; $parser]] [$($req)*] $($rest)*
+        )
+    };
+
+    (from $input:expr; $($tok:tt)*) => {
+        parse_ordered_fields!(@gen [$input] [] [] $($tok)*)
+    }
+}
+
+struct Parameter {
+    name: Ident,
+    ptype: Ident,
+    default: Expr,
+    description: LitStr,
+}
+
+impl Parse for Parameter {
+    fn parse(input: ParseStream<'_>) -> Result<Self> {
+        let name = input.parse()?;
+        input.parse::<Token![:]>()?;
+        let ptype = input.parse()?;
+
+        let fields;
+        braced!(fields in input);
+
+        parse_ordered_fields! {
+            from fields;
+            default [required] => fields.parse()?,
+            description [required] => fields.parse()?,
         }
 
-        info
+        Ok(Self {
+            name,
+            ptype,
+            default,
+            description,
+        })
     }
 }
 
-pub(crate) fn module(ts: TokenStream) -> TokenStream {
-    let mut it = ts.into_iter();
+pub(crate) struct ModuleInfo {
+    type_: Ident,
+    license: AsciiLitStr,
+    name: AsciiLitStr,
+    authors: Option<Punctuated<AsciiLitStr, Token![,]>>,
+    description: Option<LitStr>,
+    alias: Option<Punctuated<AsciiLitStr, Token![,]>>,
+    firmware: Option<Punctuated<AsciiLitStr, Token![,]>>,
+    imports_ns: Option<Punctuated<AsciiLitStr, Token![,]>>,
+    params: Option<Punctuated<Parameter, Token![,]>>,
+}
 
-    let info = ModuleInfo::parse(&mut it);
+impl Parse for ModuleInfo {
+    fn parse(input: ParseStream<'_>) -> Result<Self> {
+        parse_ordered_fields!(
+            from input;
+            type as type_ [required] => input.parse()?,
+            name [required] => input.parse()?,
+            authors => {
+                let list;
+                bracketed!(list in input);
+                Punctuated::parse_terminated(&list)?
+            },
+            description => input.parse()?,
+            license [required] => input.parse()?,
+            alias => {
+                let list;
+                bracketed!(list in input);
+                Punctuated::parse_terminated(&list)?
+            },
+            firmware => {
+                let list;
+                bracketed!(list in input);
+                Punctuated::parse_terminated(&list)?
+            },
+            imports_ns => {
+                let list;
+                bracketed!(list in input);
+                Punctuated::parse_terminated(&list)?
+            },
+            params => {
+                let list;
+                braced!(list in input);
+                Punctuated::parse_terminated(&list)?
+            },
+        );
+
+        Ok(ModuleInfo {
+            type_,
+            license,
+            name,
+            authors,
+            description,
+            alias,
+            firmware,
+            imports_ns,
+            params,
+        })
+    }
+}
 
+pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
     // Rust does not allow hyphens in identifiers, use underscore instead.
-    let ident = info.name.replace('-', "_");
+    let ident = info.name.value().replace('-', "_");
     let mut modinfo = ModInfoBuilder::new(ident.as_ref());
     if let Some(authors) = &info.authors {
         for author in authors {
-            modinfo.emit("author", author);
+            modinfo.emit("author", &author.value());
         }
     }
     if let Some(description) = &info.description {
-        modinfo.emit("description", description);
+        modinfo.emit("description", &description.value());
     }
-    modinfo.emit("license", &info.license);
+    modinfo.emit("license", &info.license.value());
     if let Some(aliases) = &info.alias {
         for alias in aliases {
-            modinfo.emit("alias", alias);
+            modinfo.emit("alias", &alias.value());
         }
     }
     if let Some(firmware) = &info.firmware {
         for fw in firmware {
-            modinfo.emit("firmware", fw);
+            modinfo.emit("firmware", &fw.value());
         }
     }
     if let Some(imports) = &info.imports_ns {
         for ns in imports {
-            modinfo.emit("import_ns", ns);
+            modinfo.emit("import_ns", &ns.value());
         }
     }
 
@@ -366,7 +459,7 @@ pub(crate) fn module(ts: TokenStream) -> TokenStream {
 
     modinfo.emit_params(&info);
 
-    format!(
+    Ok(format!(
         "
             /// The module name.
             ///
@@ -536,12 +629,12 @@ mod module_parameters {{
             }}
         ",
         type_ = info.type_,
-        name = info.name,
+        name = info.name.value(),
         ident = ident,
         modinfo = modinfo.buffer,
         params = modinfo.param_buffer,
         initcall_section = ".initcall6.init"
     )
     .parse()
-    .expect("Error parsing formatted string into token stream.")
+    .expect("Error parsing formatted string into token stream."))
 }
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 05/11] rust: macros: use `quote!` for `module!` macro
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
                   ` (3 preceding siblings ...)
  2026-01-07 16:15 ` [PATCH v2 04/11] rust: macros: use `syn` to parse `module!` macro Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 17:19   ` Tamir Duberstein
  2026-01-07 16:15 ` [PATCH v2 06/11] rust: macros: convert `#[export]` to use `syn` Gary Guo
                   ` (5 subsequent siblings)
  10 siblings, 1 reply; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Luis Chamberlain, Petr Pavlu, Daniel Gomez,
	Sami Tolvanen, Aaron Tomlin
  Cc: rust-for-linux, linux-modules, linux-kernel

From: Gary Guo <gary@garyguo.net>

This has no behavioural change, but is good for maintainability. With
`quote!`, we're no longer using string templates, so we don't need to
quote " and {} inside the template anymore. Further more, editors can
now highlight the code template.

This also improves the robustness as it eliminates the need for string
quoting and escaping.

Co-developed-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/module.rs | 558 ++++++++++++++++++++++--------------------
 1 file changed, 295 insertions(+), 263 deletions(-)

diff --git a/rust/macros/module.rs b/rust/macros/module.rs
index 6ad7b411ccde4..ba345d672839e 100644
--- a/rust/macros/module.rs
+++ b/rust/macros/module.rs
@@ -1,12 +1,15 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use std::fmt::Write;
+use std::ffi::CString;
 
 use proc_macro2::{
     Literal,
     TokenStream, //
 };
-use quote::ToTokens;
+use quote::{
+    format_ident,
+    quote, //
+};
 use syn::{
     braced,
     bracketed,
@@ -15,11 +18,13 @@
         Parse,
         ParseStream, //
     },
+    parse_quote,
     punctuated::Punctuated,
     Error,
     Expr,
     Ident,
     LitStr,
+    Path,
     Result,
     Token, //
 };
@@ -29,8 +34,8 @@
 struct ModInfoBuilder<'a> {
     module: &'a str,
     counter: usize,
-    buffer: String,
-    param_buffer: String,
+    ts: TokenStream,
+    param_ts: TokenStream,
 }
 
 impl<'a> ModInfoBuilder<'a> {
@@ -38,8 +43,8 @@ fn new(module: &'a str) -> Self {
         ModInfoBuilder {
             module,
             counter: 0,
-            buffer: String::new(),
-            param_buffer: String::new(),
+            ts: TokenStream::new(),
+            param_ts: TokenStream::new(),
         }
     }
 
@@ -56,33 +61,32 @@ fn emit_base(&mut self, field: &str, content: &str, builtin: bool, param: bool)
             // Loadable modules' modinfo strings go as-is.
             format!("{field}={content}\0")
         };
-
-        let buffer = if param {
-            &mut self.param_buffer
+        let length = string.len();
+        let string = Literal::byte_string(string.as_bytes());
+        let cfg = if builtin {
+            quote!(#[cfg(not(MODULE))])
         } else {
-            &mut self.buffer
+            quote!(#[cfg(MODULE)])
         };
 
-        write!(
-            buffer,
-            "
-                {cfg}
-                #[doc(hidden)]
-                #[cfg_attr(not(target_os = \"macos\"), link_section = \".modinfo\")]
-                #[used(compiler)]
-                pub static __{module}_{counter}: [u8; {length}] = *{string};
-            ",
-            cfg = if builtin {
-                "#[cfg(not(MODULE))]"
-            } else {
-                "#[cfg(MODULE)]"
-            },
+        let counter = format_ident!(
+            "__{module}_{counter}",
             module = self.module.to_uppercase(),
-            counter = self.counter,
-            length = string.len(),
-            string = Literal::byte_string(string.as_bytes()),
-        )
-        .unwrap();
+            counter = self.counter
+        );
+        let item = quote! {
+            #cfg
+            #[doc(hidden)]
+            #[cfg_attr(not(target_os = "macos"), link_section = ".modinfo")]
+            #[used(compiler)]
+            pub static #counter: [u8; #length] = *#string;
+        };
+
+        if param {
+            self.param_ts.extend(item);
+        } else {
+            self.ts.extend(item);
+        }
 
         self.counter += 1;
     }
@@ -115,77 +119,86 @@ fn emit_params(&mut self, info: &ModuleInfo) {
         };
 
         for param in params {
-            let param_name = param.name.to_string();
-            let param_type = param.ptype.to_string();
-            let param_default = param.default.to_token_stream().to_string();
+            let param_name_str = param.name.to_string();
+            let param_type_str = param.ptype.to_string();
 
-            let ops = param_ops_path(&param_type);
+            let ops = param_ops_path(&param_type_str);
 
             // Note: The spelling of these fields is dictated by the user space
             // tool `modinfo`.
-            self.emit_param("parmtype", &param_name, &param_type);
-            self.emit_param("parm", &param_name, &param.description.value());
-
-            write!(
-                self.param_buffer,
-                "
-                pub(crate) static {param_name}:
-                    ::kernel::module_param::ModuleParamAccess<{param_type}> =
-                        ::kernel::module_param::ModuleParamAccess::new({param_default});
-
-                const _: () = {{
-                    #[link_section = \"__param\"]
-                    #[used]
-                    static __{module_name}_{param_name}_struct:
+            self.emit_param("parmtype", &param_name_str, &param_type_str);
+            self.emit_param("parm", &param_name_str, &param.description.value());
+
+            let static_name = format_ident!("__{}_{}_struct", self.module, param.name);
+            let param_name_cstr = Literal::c_string(
+                &CString::new(param_name_str).expect("name contains NUL-terminator"),
+            );
+            let param_name_cstr_with_module = Literal::c_string(
+                &CString::new(format!("{}.{}", self.module, param.name))
+                    .expect("name contains NUL-terminator"),
+            );
+
+            let param_name = &param.name;
+            let param_type = &param.ptype;
+            let param_default = &param.default;
+
+            self.param_ts.extend(quote!{
+                #[allow(non_upper_case_globals)]
+                pub(crate) static #param_name:
+                    ::kernel::module_param::ModuleParamAccess<#param_type> =
+                        ::kernel::module_param::ModuleParamAccess::new(#param_default);
+
+                const _: () = {
+                    #[allow(non_upper_case_globals)]
+                    #[link_section = "__param"]
+                    #[used(compiler)]
+                    static #static_name:
                         ::kernel::module_param::KernelParam =
                         ::kernel::module_param::KernelParam::new(
-                            ::kernel::bindings::kernel_param {{
-                                name: if ::core::cfg!(MODULE) {{
-                                    ::kernel::c_str!(\"{param_name}\").to_bytes_with_nul()
-                                }} else {{
-                                    ::kernel::c_str!(\"{module_name}.{param_name}\")
-                                        .to_bytes_with_nul()
-                                }}.as_ptr(),
+                            ::kernel::bindings::kernel_param {
+                                name: kernel::str::as_char_ptr_in_const_context(
+                                    if ::core::cfg!(MODULE) {
+                                        #param_name_cstr
+                                    } else {
+                                        #param_name_cstr_with_module
+                                    }
+                                ),
                                 // SAFETY: `__this_module` is constructed by the kernel at load
                                 // time and will not be freed until the module is unloaded.
                                 #[cfg(MODULE)]
-                                mod_: unsafe {{
+                                mod_: unsafe {
                                     core::ptr::from_ref(&::kernel::bindings::__this_module)
                                         .cast_mut()
-                                }},
+                                },
                                 #[cfg(not(MODULE))]
                                 mod_: ::core::ptr::null_mut(),
-                                ops: core::ptr::from_ref(&{ops}),
+                                ops: core::ptr::from_ref(&#ops),
                                 perm: 0, // Will not appear in sysfs
                                 level: -1,
                                 flags: 0,
-                                __bindgen_anon_1: ::kernel::bindings::kernel_param__bindgen_ty_1 {{
-                                    arg: {param_name}.as_void_ptr()
-                                }},
-                            }}
+                                __bindgen_anon_1: ::kernel::bindings::kernel_param__bindgen_ty_1 {
+                                    arg: #param_name.as_void_ptr()
+                                },
+                            }
                         );
-                }};
-                ",
-                module_name = info.name.value(),
-                ops = ops,
-            )
-            .unwrap();
+                };
+            });
         }
     }
 }
 
-fn param_ops_path(param_type: &str) -> &'static str {
+fn param_ops_path(param_type: &str) -> Path {
     match param_type {
-        "i8" => "::kernel::module_param::PARAM_OPS_I8",
-        "u8" => "::kernel::module_param::PARAM_OPS_U8",
-        "i16" => "::kernel::module_param::PARAM_OPS_I16",
-        "u16" => "::kernel::module_param::PARAM_OPS_U16",
-        "i32" => "::kernel::module_param::PARAM_OPS_I32",
-        "u32" => "::kernel::module_param::PARAM_OPS_U32",
-        "i64" => "::kernel::module_param::PARAM_OPS_I64",
-        "u64" => "::kernel::module_param::PARAM_OPS_U64",
-        "isize" => "::kernel::module_param::PARAM_OPS_ISIZE",
-        "usize" => "::kernel::module_param::PARAM_OPS_USIZE",
+        "i8" => parse_quote!(::kernel::module_param::PARAM_OPS_I8),
+        "u8" => parse_quote!(::kernel::module_param::PARAM_OPS_U8),
+        "i16" => parse_quote!(::kernel::module_param::PARAM_OPS_I16),
+        "u16" => parse_quote!(::kernel::module_param::PARAM_OPS_U16),
+        "i32" => parse_quote!(::kernel::module_param::PARAM_OPS_I32),
+        "u32" => parse_quote!(::kernel::module_param::PARAM_OPS_U32),
+        "i64" => parse_quote!(::kernel::module_param::PARAM_OPS_I64),
+        "u64" => parse_quote!(::kernel::module_param::PARAM_OPS_U64),
+        "isize" => parse_quote!(::kernel::module_param::PARAM_OPS_ISIZE),
+        "usize" => parse_quote!(::kernel::module_param::PARAM_OPS_USIZE),
         t => panic!("Unsupported parameter type {}", t),
     }
 }
@@ -424,29 +437,41 @@ fn parse(input: ParseStream<'_>) -> Result<Self> {
 }
 
 pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
+    let ModuleInfo {
+        type_,
+        license,
+        name,
+        authors,
+        description,
+        alias,
+        firmware,
+        imports_ns,
+        params: _,
+    } = &info;
+
     // Rust does not allow hyphens in identifiers, use underscore instead.
-    let ident = info.name.value().replace('-', "_");
+    let ident = name.value().replace('-', "_");
     let mut modinfo = ModInfoBuilder::new(ident.as_ref());
-    if let Some(authors) = &info.authors {
+    if let Some(authors) = authors {
         for author in authors {
             modinfo.emit("author", &author.value());
         }
     }
-    if let Some(description) = &info.description {
+    if let Some(description) = description {
         modinfo.emit("description", &description.value());
     }
-    modinfo.emit("license", &info.license.value());
-    if let Some(aliases) = &info.alias {
+    modinfo.emit("license", &license.value());
+    if let Some(aliases) = alias {
         for alias in aliases {
             modinfo.emit("alias", &alias.value());
         }
     }
-    if let Some(firmware) = &info.firmware {
+    if let Some(firmware) = firmware {
         for fw in firmware {
             modinfo.emit("firmware", &fw.value());
         }
     }
-    if let Some(imports) = &info.imports_ns {
+    if let Some(imports) = imports_ns {
         for ns in imports {
             modinfo.emit("import_ns", &ns.value());
         }
@@ -459,182 +484,189 @@ pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
 
     modinfo.emit_params(&info);
 
-    Ok(format!(
-        "
-            /// The module name.
-            ///
-            /// Used by the printing macros, e.g. [`info!`].
-            const __LOG_PREFIX: &[u8] = b\"{name}\\0\";
-
-            // SAFETY: `__this_module` is constructed by the kernel at load time and will not be
-            // freed until the module is unloaded.
-            #[cfg(MODULE)]
-            static THIS_MODULE: ::kernel::ThisModule = unsafe {{
-                extern \"C\" {{
-                    static __this_module: ::kernel::types::Opaque<::kernel::bindings::module>;
-                }}
-
-                ::kernel::ThisModule::from_ptr(__this_module.get())
-            }};
-            #[cfg(not(MODULE))]
-            static THIS_MODULE: ::kernel::ThisModule = unsafe {{
-                ::kernel::ThisModule::from_ptr(::core::ptr::null_mut())
-            }};
-
-            /// The `LocalModule` type is the type of the module created by `module!`,
-            /// `module_pci_driver!`, `module_platform_driver!`, etc.
-            type LocalModule = {type_};
-
-            impl ::kernel::ModuleMetadata for {type_} {{
-                const NAME: &'static ::kernel::str::CStr = c\"{name}\";
-            }}
-
-            // Double nested modules, since then nobody can access the public items inside.
-            mod __module_init {{
-                mod __module_init {{
-                    use super::super::{type_};
-                    use pin_init::PinInit;
-
-                    /// The \"Rust loadable module\" mark.
-                    //
-                    // This may be best done another way later on, e.g. as a new modinfo
-                    // key or a new section. For the moment, keep it simple.
-                    #[cfg(MODULE)]
-                    #[doc(hidden)]
-                    #[used(compiler)]
-                    static __IS_RUST_MODULE: () = ();
-
-                    static mut __MOD: ::core::mem::MaybeUninit<{type_}> =
-                        ::core::mem::MaybeUninit::uninit();
-
-                    // Loadable modules need to export the `{{init,cleanup}}_module` identifiers.
-                    /// # Safety
-                    ///
-                    /// This function must not be called after module initialization, because it may be
-                    /// freed after that completes.
-                    #[cfg(MODULE)]
-                    #[doc(hidden)]
-                    #[no_mangle]
-                    #[link_section = \".init.text\"]
-                    pub unsafe extern \"C\" fn init_module() -> ::kernel::ffi::c_int {{
-                        // SAFETY: This function is inaccessible to the outside due to the double
-                        // module wrapping it. It is called exactly once by the C side via its
-                        // unique name.
-                        unsafe {{ __init() }}
-                    }}
-
-                    #[cfg(MODULE)]
-                    #[doc(hidden)]
-                    #[used(compiler)]
-                    #[link_section = \".init.data\"]
-                    static __UNIQUE_ID___addressable_init_module: unsafe extern \"C\" fn() -> i32 = init_module;
-
-                    #[cfg(MODULE)]
-                    #[doc(hidden)]
-                    #[no_mangle]
-                    #[link_section = \".exit.text\"]
-                    pub extern \"C\" fn cleanup_module() {{
-                        // SAFETY:
-                        // - This function is inaccessible to the outside due to the double
-                        //   module wrapping it. It is called exactly once by the C side via its
-                        //   unique name,
-                        // - furthermore it is only called after `init_module` has returned `0`
-                        //   (which delegates to `__init`).
-                        unsafe {{ __exit() }}
-                    }}
-
-                    #[cfg(MODULE)]
-                    #[doc(hidden)]
-                    #[used(compiler)]
-                    #[link_section = \".exit.data\"]
-                    static __UNIQUE_ID___addressable_cleanup_module: extern \"C\" fn() = cleanup_module;
-
-                    // Built-in modules are initialized through an initcall pointer
-                    // and the identifiers need to be unique.
-                    #[cfg(not(MODULE))]
-                    #[cfg(not(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS))]
-                    #[doc(hidden)]
-                    #[link_section = \"{initcall_section}\"]
-                    #[used(compiler)]
-                    pub static __{ident}_initcall: extern \"C\" fn() ->
-                        ::kernel::ffi::c_int = __{ident}_init;
-
-                    #[cfg(not(MODULE))]
-                    #[cfg(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS)]
-                    ::core::arch::global_asm!(
-                        r#\".section \"{initcall_section}\", \"a\"
-                        __{ident}_initcall:
-                            .long   __{ident}_init - .
-                            .previous
-                        \"#
-                    );
-
-                    #[cfg(not(MODULE))]
-                    #[doc(hidden)]
-                    #[no_mangle]
-                    pub extern \"C\" fn __{ident}_init() -> ::kernel::ffi::c_int {{
-                        // SAFETY: This function is inaccessible to the outside due to the double
-                        // module wrapping it. It is called exactly once by the C side via its
-                        // placement above in the initcall section.
-                        unsafe {{ __init() }}
-                    }}
-
-                    #[cfg(not(MODULE))]
-                    #[doc(hidden)]
-                    #[no_mangle]
-                    pub extern \"C\" fn __{ident}_exit() {{
-                        // SAFETY:
-                        // - This function is inaccessible to the outside due to the double
-                        //   module wrapping it. It is called exactly once by the C side via its
-                        //   unique name,
-                        // - furthermore it is only called after `__{ident}_init` has
-                        //   returned `0` (which delegates to `__init`).
-                        unsafe {{ __exit() }}
-                    }}
-
-                    /// # Safety
-                    ///
-                    /// This function must only be called once.
-                    unsafe fn __init() -> ::kernel::ffi::c_int {{
-                        let initer =
-                            <{type_} as ::kernel::InPlaceModule>::init(&super::super::THIS_MODULE);
-                        // SAFETY: No data race, since `__MOD` can only be accessed by this module
-                        // and there only `__init` and `__exit` access it. These functions are only
-                        // called once and `__exit` cannot be called before or during `__init`.
-                        match unsafe {{ initer.__pinned_init(__MOD.as_mut_ptr()) }} {{
-                            Ok(m) => 0,
-                            Err(e) => e.to_errno(),
-                        }}
-                    }}
-
-                    /// # Safety
-                    ///
-                    /// This function must
-                    /// - only be called once,
-                    /// - be called after `__init` has been called and returned `0`.
-                    unsafe fn __exit() {{
-                        // SAFETY: No data race, since `__MOD` can only be accessed by this module
-                        // and there only `__init` and `__exit` access it. These functions are only
-                        // called once and `__init` was already called.
-                        unsafe {{
-                            // Invokes `drop()` on `__MOD`, which should be used for cleanup.
-                            __MOD.assume_init_drop();
-                        }}
-                    }}
-                    {modinfo}
-                }}
-            }}
-            mod module_parameters {{
-                {params}
-            }}
-        ",
-        type_ = info.type_,
-        name = info.name.value(),
-        ident = ident,
-        modinfo = modinfo.buffer,
-        params = modinfo.param_buffer,
-        initcall_section = ".initcall6.init"
-    )
-    .parse()
-    .expect("Error parsing formatted string into token stream."))
+    let modinfo_ts = modinfo.ts;
+    let params_ts = modinfo.param_ts;
+
+    let ident_init = format_ident!("__{ident}_init");
+    let ident_exit = format_ident!("__{ident}_exit");
+    let ident_initcall = format_ident!("__{ident}_initcall");
+    let initcall_section = ".initcall6.init";
+
+    let global_asm = format!(
+        r#".section "{initcall_section}", "a"
+        __{ident}_initcall:
+            .long   __{ident}_init - .
+            .previous
+        "#
+    );
+
+    let name_cstr =
+        Literal::c_string(&CString::new(name.value()).expect("name contains NUL-terminator"));
+
+    Ok(quote! {
+        /// The module name.
+        ///
+        /// Used by the printing macros, e.g. [`info!`].
+        const __LOG_PREFIX: &[u8] = #name_cstr.to_bytes_with_nul();
+
+        // SAFETY: `__this_module` is constructed by the kernel at load time and will not be
+        // freed until the module is unloaded.
+        #[cfg(MODULE)]
+        static THIS_MODULE: ::kernel::ThisModule = unsafe {
+            extern "C" {
+                static __this_module: ::kernel::types::Opaque<::kernel::bindings::module>;
+            };
+
+            ::kernel::ThisModule::from_ptr(__this_module.get())
+        };
+
+        #[cfg(not(MODULE))]
+        static THIS_MODULE: ::kernel::ThisModule = unsafe {
+            ::kernel::ThisModule::from_ptr(::core::ptr::null_mut())
+        };
+
+        /// The `LocalModule` type is the type of the module created by `module!`,
+        /// `module_pci_driver!`, `module_platform_driver!`, etc.
+        type LocalModule = #type_;
+
+        impl ::kernel::ModuleMetadata for #type_ {
+            const NAME: &'static ::kernel::str::CStr = #name_cstr;
+        }
+
+        // Double nested modules, since then nobody can access the public items inside.
+        mod __module_init {
+            mod __module_init {
+                use super::super::#type_;
+                use pin_init::PinInit;
+
+                /// The "Rust loadable module" mark.
+                //
+                // This may be best done another way later on, e.g. as a new modinfo
+                // key or a new section. For the moment, keep it simple.
+                #[cfg(MODULE)]
+                #[doc(hidden)]
+                #[used(compiler)]
+                static __IS_RUST_MODULE: () = ();
+
+                static mut __MOD: ::core::mem::MaybeUninit<#type_> =
+                    ::core::mem::MaybeUninit::uninit();
+
+                // Loadable modules need to export the `{init,cleanup}_module` identifiers.
+                /// # Safety
+                ///
+                /// This function must not be called after module initialization, because it may be
+                /// freed after that completes.
+                #[cfg(MODULE)]
+                #[doc(hidden)]
+                #[no_mangle]
+                #[link_section = ".init.text"]
+                pub unsafe extern "C" fn init_module() -> ::kernel::ffi::c_int {
+                    // SAFETY: This function is inaccessible to the outside due to the double
+                    // module wrapping it. It is called exactly once by the C side via its
+                    // unique name.
+                    unsafe { __init() }
+                }
+
+                #[cfg(MODULE)]
+                #[doc(hidden)]
+                #[used(compiler)]
+                #[link_section = ".init.data"]
+                static __UNIQUE_ID___addressable_init_module: unsafe extern "C" fn() -> i32 =
+                    init_module;
+
+                #[cfg(MODULE)]
+                #[doc(hidden)]
+                #[no_mangle]
+                #[link_section = ".exit.text"]
+                pub extern "C" fn cleanup_module() {
+                    // SAFETY:
+                    // - This function is inaccessible to the outside due to the double
+                    //   module wrapping it. It is called exactly once by the C side via its
+                    //   unique name,
+                    // - furthermore it is only called after `init_module` has returned `0`
+                    //   (which delegates to `__init`).
+                    unsafe { __exit() }
+                }
+
+                #[cfg(MODULE)]
+                #[doc(hidden)]
+                #[used(compiler)]
+                #[link_section = ".exit.data"]
+                static __UNIQUE_ID___addressable_cleanup_module: extern "C" fn() = cleanup_module;
+
+                // Built-in modules are initialized through an initcall pointer
+                // and the identifiers need to be unique.
+                #[cfg(not(MODULE))]
+                #[cfg(not(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS))]
+                #[doc(hidden)]
+                #[link_section = #initcall_section]
+                #[used(compiler)]
+                pub static #ident_initcall: extern "C" fn() ->
+                    ::kernel::ffi::c_int = #ident_initcall;
+
+                #[cfg(not(MODULE))]
+                #[cfg(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS)]
+                ::core::arch::global_asm!(#global_asm);
+
+                #[cfg(not(MODULE))]
+                #[doc(hidden)]
+                #[no_mangle]
+                pub extern "C" fn #ident_init() -> ::kernel::ffi::c_int {
+                    // SAFETY: This function is inaccessible to the outside due to the double
+                    // module wrapping it. It is called exactly once by the C side via its
+                    // placement above in the initcall section.
+                    unsafe { __init() }
+                }
+
+                #[cfg(not(MODULE))]
+                #[doc(hidden)]
+                #[no_mangle]
+                pub extern "C" fn #ident_exit() {
+                    // SAFETY:
+                    // - This function is inaccessible to the outside due to the double
+                    //   module wrapping it. It is called exactly once by the C side via its
+                    //   unique name,
+                    // - furthermore it is only called after `#ident_init` has
+                    //   returned `0` (which delegates to `__init`).
+                    unsafe { __exit() }
+                }
+
+                /// # Safety
+                ///
+                /// This function must only be called once.
+                unsafe fn __init() -> ::kernel::ffi::c_int {
+                    let initer =
+                        <#type_ as ::kernel::InPlaceModule>::init(&super::super::THIS_MODULE);
+                    // SAFETY: No data race, since `__MOD` can only be accessed by this module
+                    // and there only `__init` and `__exit` access it. These functions are only
+                    // called once and `__exit` cannot be called before or during `__init`.
+                    match unsafe { initer.__pinned_init(__MOD.as_mut_ptr()) } {
+                        Ok(m) => 0,
+                        Err(e) => e.to_errno(),
+                    }
+                }
+
+                /// # Safety
+                ///
+                /// This function must
+                /// - only be called once,
+                /// - be called after `__init` has been called and returned `0`.
+                unsafe fn __exit() {
+                    // SAFETY: No data race, since `__MOD` can only be accessed by this module
+                    // and there only `__init` and `__exit` access it. These functions are only
+                    // called once and `__init` was already called.
+                    unsafe {
+                        // Invokes `drop()` on `__MOD`, which should be used for cleanup.
+                        __MOD.assume_init_drop();
+                    }
+                }
+
+                #modinfo_ts
+            }
+        }
+
+        mod module_parameters {
+            #params_ts
+        }
+    })
 }
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 06/11] rust: macros: convert `#[export]` to use `syn`
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
                   ` (4 preceding siblings ...)
  2026-01-07 16:15 ` [PATCH v2 05/11] rust: macros: use `quote!` for " Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 16:15 ` [PATCH v2 07/11] rust: macros: convert `concat_idents!` " Gary Guo
                   ` (4 subsequent siblings)
  10 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Tamir Duberstein, Guilherme Giacomo Simoes,
	José Expósito
  Cc: rust-for-linux, linux-kernel

From: Gary Guo <gary@garyguo.net>

This eliminates the custom `function_name` helper.

Reviewed-by: Tamir Duberstein <tamird@gmail.com>
Reviewed-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/export.rs  | 24 +++++++++---------------
 rust/macros/helpers.rs | 18 ------------------
 rust/macros/lib.rs     |  5 +++--
 3 files changed, 12 insertions(+), 35 deletions(-)

diff --git a/rust/macros/export.rs b/rust/macros/export.rs
index 92d9b30971929..6d53521f62fc7 100644
--- a/rust/macros/export.rs
+++ b/rust/macros/export.rs
@@ -3,19 +3,14 @@
 use proc_macro2::TokenStream;
 use quote::quote;
 
-use crate::helpers::function_name;
-
 /// Please see [`crate::export`] for documentation.
-pub(crate) fn export(_attr: TokenStream, ts: TokenStream) -> TokenStream {
-    let Some(name) = function_name(ts.clone()) else {
-        return "::core::compile_error!(\"The #[export] attribute must be used on a function.\");"
-            .parse::<TokenStream>()
-            .unwrap();
-    };
+pub(crate) fn export(f: syn::ItemFn) -> TokenStream {
+    let name = &f.sig.ident;
 
-    // This verifies that the function has the same signature as the declaration generated by
-    // bindgen. It makes use of the fact that all branches of an if/else must have the same type.
-    let signature_check = quote!(
+    quote! {
+        // This verifies that the function has the same signature as the declaration generated by
+        // bindgen. It makes use of the fact that all branches of an if/else must have the same
+        // type.
         const _: () = {
             if true {
                 ::kernel::bindings::#name
@@ -23,9 +18,8 @@ pub(crate) fn export(_attr: TokenStream, ts: TokenStream) -> TokenStream {
                 #name
             };
         };
-    );
-
-    let no_mangle = quote!(#[no_mangle]);
 
-    TokenStream::from_iter([signature_check, no_mangle, ts])
+        #[no_mangle]
+        #f
+    }
 }
diff --git a/rust/macros/helpers.rs b/rust/macros/helpers.rs
index fa66ef6eb0f3d..754c827cc21e1 100644
--- a/rust/macros/helpers.rs
+++ b/rust/macros/helpers.rs
@@ -2,7 +2,6 @@
 
 use proc_macro2::{
     token_stream,
-    Ident,
     TokenStream,
     TokenTree, //
 };
@@ -50,23 +49,6 @@ pub(crate) fn value(&self) -> String {
     }
 }
 
-/// Given a function declaration, finds the name of the function.
-pub(crate) fn function_name(input: TokenStream) -> Option<Ident> {
-    let mut input = input.into_iter();
-    while let Some(token) = input.next() {
-        match token {
-            TokenTree::Ident(i) if i == "fn" => {
-                if let Some(TokenTree::Ident(i)) = input.next() {
-                    return Some(i);
-                }
-                return None;
-            }
-            _ => continue,
-        }
-    }
-    None
-}
-
 pub(crate) fn file() -> String {
     #[cfg(not(CONFIG_RUSTC_HAS_SPAN_FILE))]
     {
diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
index c5347127a3a51..da8239d2474b6 100644
--- a/rust/macros/lib.rs
+++ b/rust/macros/lib.rs
@@ -234,8 +234,9 @@ pub fn vtable(attr: TokenStream, input: TokenStream) -> TokenStream {
 /// This macro is *not* the same as the C macros `EXPORT_SYMBOL_*`. All Rust symbols are currently
 /// automatically exported with `EXPORT_SYMBOL_GPL`.
 #[proc_macro_attribute]
-pub fn export(attr: TokenStream, ts: TokenStream) -> TokenStream {
-    export::export(attr.into(), ts.into()).into()
+pub fn export(attr: TokenStream, input: TokenStream) -> TokenStream {
+    parse_macro_input!(attr as syn::parse::Nothing);
+    export::export(parse_macro_input!(input)).into()
 }
 
 /// Like [`core::format_args!`], but automatically wraps arguments in [`kernel::fmt::Adapter`].
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 07/11] rust: macros: convert `concat_idents!` to use `syn`
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
                   ` (5 preceding siblings ...)
  2026-01-07 16:15 ` [PATCH v2 06/11] rust: macros: convert `#[export]` to use `syn` Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 16:15 ` [PATCH v2 08/11] rust: macros: convert `#[kunit_tests]` macro " Gary Guo
                   ` (3 subsequent siblings)
  10 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Tamir Duberstein, Igor Korotin,
	José Expósito
  Cc: rust-for-linux, linux-kernel

From: Gary Guo <gary@garyguo.net>

This eliminates the need for `expect_punct` helper.

Reviewed-by: Tamir Duberstein <tamird@gmail.com>
Reviewed-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/concat_idents.rs | 39 ++++++++++++++++++++++++------------
 rust/macros/helpers.rs       | 14 +------------
 rust/macros/lib.rs           |  4 ++--
 3 files changed, 29 insertions(+), 28 deletions(-)

diff --git a/rust/macros/concat_idents.rs b/rust/macros/concat_idents.rs
index 12cb231c3d715..47b6add378d2c 100644
--- a/rust/macros/concat_idents.rs
+++ b/rust/macros/concat_idents.rs
@@ -1,23 +1,36 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use proc_macro2::{token_stream, Ident, TokenStream, TokenTree};
+use proc_macro2::{
+    Ident,
+    TokenStream,
+    TokenTree, //
+};
+use syn::{
+    parse::{
+        Parse,
+        ParseStream, //
+    },
+    Result,
+    Token, //
+};
 
-use crate::helpers::expect_punct;
+pub(crate) struct Input {
+    a: Ident,
+    _comma: Token![,],
+    b: Ident,
+}
 
-fn expect_ident(it: &mut token_stream::IntoIter) -> Ident {
-    if let Some(TokenTree::Ident(ident)) = it.next() {
-        ident
-    } else {
-        panic!("Expected Ident")
+impl Parse for Input {
+    fn parse(input: ParseStream<'_>) -> Result<Self> {
+        Ok(Self {
+            a: input.parse()?,
+            _comma: input.parse()?,
+            b: input.parse()?,
+        })
     }
 }
 
-pub(crate) fn concat_idents(ts: TokenStream) -> TokenStream {
-    let mut it = ts.into_iter();
-    let a = expect_ident(&mut it);
-    assert_eq!(expect_punct(&mut it), ',');
-    let b = expect_ident(&mut it);
-    assert!(it.next().is_none(), "only two idents can be concatenated");
+pub(crate) fn concat_idents(Input { a, b, .. }: Input) -> TokenStream {
     let res = Ident::new(&format!("{a}{b}"), b.span());
     TokenStream::from_iter([TokenTree::Ident(res)])
 }
diff --git a/rust/macros/helpers.rs b/rust/macros/helpers.rs
index 754c827cc21e1..adfa60d8f42d8 100644
--- a/rust/macros/helpers.rs
+++ b/rust/macros/helpers.rs
@@ -1,10 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0
 
-use proc_macro2::{
-    token_stream,
-    TokenStream,
-    TokenTree, //
-};
+use proc_macro2::TokenStream;
 use quote::ToTokens;
 use syn::{
     parse::{
@@ -16,14 +12,6 @@
     Result, //
 };
 
-pub(crate) fn expect_punct(it: &mut token_stream::IntoIter) -> char {
-    if let TokenTree::Punct(punct) = it.next().expect("Reached end of token stream for Punct") {
-        punct.as_char()
-    } else {
-        panic!("Expected Punct");
-    }
-}
-
 /// A string literal that is required to have ASCII value only.
 pub(crate) struct AsciiLitStr(LitStr);
 
diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
index da8239d2474b6..12467bfc703a8 100644
--- a/rust/macros/lib.rs
+++ b/rust/macros/lib.rs
@@ -311,8 +311,8 @@ pub fn fmt(input: TokenStream) -> TokenStream {
 /// assert_eq!(BR_OK, binder_driver_return_protocol_BR_OK);
 /// ```
 #[proc_macro]
-pub fn concat_idents(ts: TokenStream) -> TokenStream {
-    concat_idents::concat_idents(ts.into()).into()
+pub fn concat_idents(input: TokenStream) -> TokenStream {
+    concat_idents::concat_idents(parse_macro_input!(input)).into()
 }
 
 /// Paste identifiers together.
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 08/11] rust: macros: convert `#[kunit_tests]` macro to use `syn`
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
                   ` (6 preceding siblings ...)
  2026-01-07 16:15 ` [PATCH v2 07/11] rust: macros: convert `concat_idents!` " Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 17:22   ` Tamir Duberstein
  2026-01-07 16:15 ` [PATCH v2 09/11] rust: macros: allow arbitrary types to be used in `module!` macro Gary Guo
                   ` (2 subsequent siblings)
  10 siblings, 1 reply; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Brendan Higgins, David Gow, Rae Moar,
	Tamir Duberstein, José Expósito
  Cc: rust-for-linux, Igor Korotin, linux-kselftest, kunit-dev,
	linux-kernel

From: Gary Guo <gary@garyguo.net>

Make use of `syn` to parse the module structurally and thus improve the
robustness of parsing.

String interpolation is avoided by generating tokens directly using
`quote!`.

Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/kunit.rs | 274 +++++++++++++++++++------------------------
 rust/macros/lib.rs   |   6 +-
 2 files changed, 123 insertions(+), 157 deletions(-)

diff --git a/rust/macros/kunit.rs b/rust/macros/kunit.rs
index 5cd6aa5eef07d..afbc708cbdc50 100644
--- a/rust/macros/kunit.rs
+++ b/rust/macros/kunit.rs
@@ -4,81 +4,50 @@
 //!
 //! Copyright (c) 2023 José Expósito <jose.exposito89@gmail.com>
 
-use std::collections::HashMap;
-use std::fmt::Write;
-
-use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
-
-pub(crate) fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
-    let attr = attr.to_string();
-
-    if attr.is_empty() {
-        panic!("Missing test name in `#[kunit_tests(test_name)]` macro")
-    }
-
-    if attr.len() > 255 {
-        panic!("The test suite name `{attr}` exceeds the maximum length of 255 bytes")
+use std::ffi::CString;
+
+use proc_macro2::TokenStream;
+use quote::{
+    format_ident,
+    quote,
+    ToTokens, //
+};
+use syn::{
+    parse_quote,
+    Error,
+    Ident,
+    Item,
+    ItemMod,
+    LitCStr,
+    Result, //
+};
+
+pub(crate) fn kunit_tests(test_suite: Ident, mut module: ItemMod) -> Result<TokenStream> {
+    if test_suite.to_string().len() > 255 {
+        return Err(Error::new_spanned(
+            test_suite,
+            "test suite names cannot exceed the maximum length of 255 bytes",
+        ));
     }
 
-    let mut tokens: Vec<_> = ts.into_iter().collect();
-
-    // Scan for the `mod` keyword.
-    tokens
-        .iter()
-        .find_map(|token| match token {
-            TokenTree::Ident(ident) => match ident.to_string().as_str() {
-                "mod" => Some(true),
-                _ => None,
-            },
-            _ => None,
-        })
-        .expect("`#[kunit_tests(test_name)]` attribute should only be applied to modules");
-
-    // Retrieve the main body. The main body should be the last token tree.
-    let body = match tokens.pop() {
-        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => group,
-        _ => panic!("Cannot locate main body of module"),
+    // We cannot handle modules that defer to another file (e.g. `mod foo;`).
+    let Some((module_brace, module_items)) = module.content.take() else {
+        Err(Error::new_spanned(
+            module,
+            "`#[kunit_tests(test_name)]` attribute should only be applied to inline modules",
+        ))?
     };
 
-    // Get the functions set as tests. Search for `[test]` -> `fn`.
-    let mut body_it = body.stream().into_iter();
-    let mut tests = Vec::new();
-    let mut attributes: HashMap<String, TokenStream> = HashMap::new();
-    while let Some(token) = body_it.next() {
-        match token {
-            TokenTree::Punct(ref p) if p.as_char() == '#' => match body_it.next() {
-                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => {
-                    if let Some(TokenTree::Ident(name)) = g.stream().into_iter().next() {
-                        // Collect attributes because we need to find which are tests. We also
-                        // need to copy `cfg` attributes so tests can be conditionally enabled.
-                        attributes
-                            .entry(name.to_string())
-                            .or_default()
-                            .extend([token, TokenTree::Group(g)]);
-                    }
-                    continue;
-                }
-                _ => (),
-            },
-            TokenTree::Ident(i) if i == "fn" && attributes.contains_key("test") => {
-                if let Some(TokenTree::Ident(test_name)) = body_it.next() {
-                    tests.push((test_name, attributes.remove("cfg").unwrap_or_default()))
-                }
-            }
-
-            _ => (),
-        }
-        attributes.clear();
-    }
+    // Make the entire module gated behind `CONFIG_KUNIT`.
+    module
+        .attrs
+        .insert(0, parse_quote!(#[cfg(CONFIG_KUNIT="y")]));
 
-    // Add `#[cfg(CONFIG_KUNIT="y")]` before the module declaration.
-    let config_kunit = "#[cfg(CONFIG_KUNIT=\"y\")]".to_owned().parse().unwrap();
-    tokens.insert(
-        0,
-        TokenTree::Group(Group::new(Delimiter::None, config_kunit)),
-    );
+    let mut processed_items = Vec::new();
+    let mut test_cases = Vec::new();
 
     // Generate the test KUnit test suite and a test case for each `#[test]`.
+    //
     // The code generated for the following test module:
     //
     // ```
@@ -110,98 +79,93 @@ pub(crate) fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
     //
     // ::kernel::kunit_unsafe_test_suite!(kunit_test_suit_name, TEST_CASES);
     // ```
-    let mut kunit_macros = "".to_owned();
-    let mut test_cases = "".to_owned();
-    let mut assert_macros = "".to_owned();
-    let path = crate::helpers::file();
-    let num_tests = tests.len();
-    for (test, cfg_attr) in tests {
-        let kunit_wrapper_fn_name = format!("kunit_rust_wrapper_{test}");
-        // Append any `cfg` attributes the user might have written on their tests so we don't
-        // attempt to call them when they are `cfg`'d out. An extra `use` is used here to reduce
-        // the length of the assert message.
-        let kunit_wrapper = format!(
-            r#"unsafe extern "C" fn {kunit_wrapper_fn_name}(_test: *mut ::kernel::bindings::kunit)
-            {{
-                (*_test).status = ::kernel::bindings::kunit_status_KUNIT_SKIPPED;
-                {cfg_attr} {{
-                    (*_test).status = ::kernel::bindings::kunit_status_KUNIT_SUCCESS;
-                    use ::kernel::kunit::is_test_result_ok;
-                    assert!(is_test_result_ok({test}()));
+    //
+    // Non-function items (e.g. imports) are preserved.
+    for item in module_items {
+        let Item::Fn(mut f) = item else {
+            processed_items.push(item);
+            continue;
+        };
+
+        // TODO: Replace below with `extract_if` when MSRV is bumped above 1.85.
+        let before_len = f.attrs.len();
+        f.attrs.retain(|attr| !attr.path().is_ident("test"));
+        if f.attrs.len() == before_len {
+            processed_items.push(Item::Fn(f));
+            continue;
+        }
+
+        let test = f.sig.ident.clone();
+
+        // Retrieve `#[cfg]` applied on the function which needs to be present on derived items too.
+        let cfg_attrs: Vec<_> = f
+            .attrs
+            .iter()
+            .filter(|attr| attr.path().is_ident("cfg"))
+            .cloned()
+            .collect();
+
+        // Before the test, override usual `assert!` and `assert_eq!` macros with ones that call
+        // KUnit instead.
+        let test_str = test.to_string();
+        let path = crate::helpers::file();
+        processed_items.push(parse_quote! {
+            #[allow(unused)]
+            macro_rules! assert {
+                ($cond:expr $(,)?) => {{
+                    kernel::kunit_assert!(#test_str, #path, 0, $cond);
+                }}
+            }
+        });
+        processed_items.push(parse_quote! {
+            #[allow(unused)]
+            macro_rules! assert_eq {
+                ($left:expr, $right:expr $(,)?) => {{
+                    kernel::kunit_assert_eq!(#test_str, #path, 0, $left, $right);
                 }}
-            }}"#,
+            }
+        });
+
+        // Add back the test item.
+        processed_items.push(Item::Fn(f));
+
+        let kunit_wrapper_fn_name = format_ident!("kunit_rust_wrapper_{test}");
+        let test_cstr = LitCStr::new(
+            &CString::new(test_str.as_str()).expect("identifier cannot contain NUL"),
+            test.span(),
         );
-        writeln!(kunit_macros, "{kunit_wrapper}").unwrap();
-        writeln!(
-            test_cases,
-            "    ::kernel::kunit::kunit_case(::kernel::c_str!(\"{test}\"), {kunit_wrapper_fn_name}),"
-        )
-        .unwrap();
-        writeln!(
-            assert_macros,
-            r#"
-/// Overrides the usual [`assert!`] macro with one that calls KUnit instead.
-#[allow(unused)]
-macro_rules! assert {{
-    ($cond:expr $(,)?) => {{{{
-        kernel::kunit_assert!("{test}", "{path}", 0, $cond);
-    }}}}
-}}
-
-/// Overrides the usual [`assert_eq!`] macro with one that calls KUnit instead.
-#[allow(unused)]
-macro_rules! assert_eq {{
-    ($left:expr, $right:expr $(,)?) => {{{{
-        kernel::kunit_assert_eq!("{test}", "{path}", 0, $left, $right);
-    }}}}
-}}
-        "#
-        )
-        .unwrap();
-    }
+        processed_items.push(parse_quote! {
+            unsafe extern "C" fn #kunit_wrapper_fn_name(_test: *mut ::kernel::bindings::kunit) {
+                (*_test).status = ::kernel::bindings::kunit_status_KUNIT_SKIPPED;
 
-    writeln!(kunit_macros).unwrap();
-    writeln!(
-        kunit_macros,
-        "static mut TEST_CASES: [::kernel::bindings::kunit_case; {}] = [\n{test_cases}    ::kernel::kunit::kunit_case_null(),\n];",
-        num_tests + 1
-    )
-    .unwrap();
-
-    writeln!(
-        kunit_macros,
-        "::kernel::kunit_unsafe_test_suite!({attr}, TEST_CASES);"
-    )
-    .unwrap();
-
-    // Remove the `#[test]` macros.
-    // We do this at a token level, in order to preserve span information.
-    let mut new_body = vec![];
-    let mut body_it = body.stream().into_iter();
-
-    while let Some(token) = body_it.next() {
-        match token {
-            TokenTree::Punct(ref c) if c.as_char() == '#' => match body_it.next() {
-                Some(TokenTree::Group(group)) if group.to_string() == "[test]" => (),
-                Some(next) => {
-                    new_body.extend([token, next]);
-                }
-                _ => {
-                    new_body.push(token);
+                // Append any `cfg` attributes the user might have written on their tests so we
+                // don't attempt to call them when they are `cfg`'d out. An extra `use` is used
+                // here to reduce the length of the assert message.
+                #(#cfg_attrs)*
+                {
+                    (*_test).status = ::kernel::bindings::kunit_status_KUNIT_SUCCESS;
+                    use ::kernel::kunit::is_test_result_ok;
+                    assert!(is_test_result_ok(#test()));
                 }
-            },
-            _ => {
-                new_body.push(token);
             }
-        }
-    }
+        });
 
-    let mut final_body = TokenStream::new();
-    final_body.extend::<TokenStream>(assert_macros.parse().unwrap());
-    final_body.extend(new_body);
-    final_body.extend::<TokenStream>(kunit_macros.parse().unwrap());
-
-    tokens.push(TokenTree::Group(Group::new(Delimiter::Brace, final_body)));
+        test_cases.push(quote!(
+            ::kernel::kunit::kunit_case(#test_cstr, #kunit_wrapper_fn_name)
+        ));
+    }
 
-    tokens.into_iter().collect()
+    let num_tests_plus_1 = test_cases.len() + 1;
+    processed_items.push(parse_quote! {
+        static mut TEST_CASES: [::kernel::bindings::kunit_case; #num_tests_plus_1] = [
+            #(#test_cases,)*
+            ::kernel::kunit::kunit_case_null(),
+        ];
+    });
+    processed_items.push(parse_quote! {
+        ::kernel::kunit_unsafe_test_suite!(#test_suite, TEST_CASES);
+    });
+
+    module.content = Some((module_brace, processed_items));
+    Ok(module.to_token_stream())
 }
diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
index 12467bfc703a8..75ac60abe6ffa 100644
--- a/rust/macros/lib.rs
+++ b/rust/macros/lib.rs
@@ -481,6 +481,8 @@ pub fn paste(input: TokenStream) -> TokenStream {
 /// }
 /// ```
 #[proc_macro_attribute]
-pub fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
-    kunit::kunit_tests(attr.into(), ts.into()).into()
+pub fn kunit_tests(attr: TokenStream, input: TokenStream) -> TokenStream {
+    kunit::kunit_tests(parse_macro_input!(attr), parse_macro_input!(input))
+        .unwrap_or_else(|e| e.into_compile_error())
+        .into()
 }
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 09/11] rust: macros: allow arbitrary types to be used in `module!` macro
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
                   ` (7 preceding siblings ...)
  2026-01-07 16:15 ` [PATCH v2 08/11] rust: macros: convert `#[kunit_tests]` macro " Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 16:15 ` [PATCH v2 10/11] rust: macros: rearrange `#[doc(hidden)]` " Gary Guo
  2026-01-07 16:15 ` [PATCH v2 11/11] rust: kunit: use `pin_init::zeroed` instead of custom null value Gary Guo
  10 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Luis Chamberlain, Petr Pavlu, Daniel Gomez,
	Sami Tolvanen, Aaron Tomlin
  Cc: rust-for-linux, linux-modules, linux-kernel

From: Gary Guo <gary@garyguo.net>

Previously this only accepts an identifier, but now with `syn` it is
easy to make it accepts any type.

Reviewed-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/module.rs | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/rust/macros/module.rs b/rust/macros/module.rs
index ba345d672839e..31d39764c6926 100644
--- a/rust/macros/module.rs
+++ b/rust/macros/module.rs
@@ -26,7 +26,8 @@
     LitStr,
     Path,
     Result,
-    Token, //
+    Token,
+    Type, //
 };
 
 use crate::helpers::*;
@@ -376,7 +377,7 @@ fn parse(input: ParseStream<'_>) -> Result<Self> {
 }
 
 pub(crate) struct ModuleInfo {
-    type_: Ident,
+    type_: Type,
     license: AsciiLitStr,
     name: AsciiLitStr,
     authors: Option<Punctuated<AsciiLitStr, Token![,]>>,
@@ -536,7 +537,6 @@ impl ::kernel::ModuleMetadata for #type_ {
         // Double nested modules, since then nobody can access the public items inside.
         mod __module_init {
             mod __module_init {
-                use super::super::#type_;
                 use pin_init::PinInit;
 
                 /// The "Rust loadable module" mark.
@@ -548,7 +548,7 @@ mod __module_init {
                 #[used(compiler)]
                 static __IS_RUST_MODULE: () = ();
 
-                static mut __MOD: ::core::mem::MaybeUninit<#type_> =
+                static mut __MOD: ::core::mem::MaybeUninit<super::super::LocalModule> =
                     ::core::mem::MaybeUninit::uninit();
 
                 // Loadable modules need to export the `{init,cleanup}_module` identifiers.
@@ -635,8 +635,9 @@ pub extern "C" fn #ident_exit() {
                 ///
                 /// This function must only be called once.
                 unsafe fn __init() -> ::kernel::ffi::c_int {
-                    let initer =
-                        <#type_ as ::kernel::InPlaceModule>::init(&super::super::THIS_MODULE);
+                    let initer = <super::super::LocalModule as ::kernel::InPlaceModule>::init(
+                        &super::super::THIS_MODULE
+                    );
                     // SAFETY: No data race, since `__MOD` can only be accessed by this module
                     // and there only `__init` and `__exit` access it. These functions are only
                     // called once and `__exit` cannot be called before or during `__init`.
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 10/11] rust: macros: rearrange `#[doc(hidden)]` in `module!` macro
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
                   ` (8 preceding siblings ...)
  2026-01-07 16:15 ` [PATCH v2 09/11] rust: macros: allow arbitrary types to be used in `module!` macro Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  2026-01-07 16:15 ` [PATCH v2 11/11] rust: kunit: use `pin_init::zeroed` instead of custom null value Gary Guo
  10 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Luis Chamberlain, Petr Pavlu, Daniel Gomez,
	Sami Tolvanen, Aaron Tomlin
  Cc: rust-for-linux, Tamir Duberstein, linux-modules, linux-kernel

From: Gary Guo <gary@garyguo.net>

This `#[doc(hidden)]` can just be applied on a module to hide anything
within.

Reviewed-by: Tamir Duberstein <tamird@gmail.com>
Reviewed-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/macros/module.rs | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/rust/macros/module.rs b/rust/macros/module.rs
index 31d39764c6926..c86cced5e0e8f 100644
--- a/rust/macros/module.rs
+++ b/rust/macros/module.rs
@@ -77,7 +77,6 @@ fn emit_base(&mut self, field: &str, content: &str, builtin: bool, param: bool)
         );
         let item = quote! {
             #cfg
-            #[doc(hidden)]
             #[cfg_attr(not(target_os = "macos"), link_section = ".modinfo")]
             #[used(compiler)]
             pub static #counter: [u8; #length] = *#string;
@@ -535,6 +534,7 @@ impl ::kernel::ModuleMetadata for #type_ {
         }
 
         // Double nested modules, since then nobody can access the public items inside.
+        #[doc(hidden)]
         mod __module_init {
             mod __module_init {
                 use pin_init::PinInit;
@@ -544,7 +544,6 @@ mod __module_init {
                 // This may be best done another way later on, e.g. as a new modinfo
                 // key or a new section. For the moment, keep it simple.
                 #[cfg(MODULE)]
-                #[doc(hidden)]
                 #[used(compiler)]
                 static __IS_RUST_MODULE: () = ();
 
@@ -557,7 +556,6 @@ mod __module_init {
                 /// This function must not be called after module initialization, because it may be
                 /// freed after that completes.
                 #[cfg(MODULE)]
-                #[doc(hidden)]
                 #[no_mangle]
                 #[link_section = ".init.text"]
                 pub unsafe extern "C" fn init_module() -> ::kernel::ffi::c_int {
@@ -568,14 +566,12 @@ mod __module_init {
                 }
 
                 #[cfg(MODULE)]
-                #[doc(hidden)]
                 #[used(compiler)]
                 #[link_section = ".init.data"]
                 static __UNIQUE_ID___addressable_init_module: unsafe extern "C" fn() -> i32 =
                     init_module;
 
                 #[cfg(MODULE)]
-                #[doc(hidden)]
                 #[no_mangle]
                 #[link_section = ".exit.text"]
                 pub extern "C" fn cleanup_module() {
@@ -589,7 +585,6 @@ pub extern "C" fn cleanup_module() {
                 }
 
                 #[cfg(MODULE)]
-                #[doc(hidden)]
                 #[used(compiler)]
                 #[link_section = ".exit.data"]
                 static __UNIQUE_ID___addressable_cleanup_module: extern "C" fn() = cleanup_module;
@@ -598,7 +593,6 @@ pub extern "C" fn cleanup_module() {
                 // and the identifiers need to be unique.
                 #[cfg(not(MODULE))]
                 #[cfg(not(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS))]
-                #[doc(hidden)]
                 #[link_section = #initcall_section]
                 #[used(compiler)]
                 pub static #ident_initcall: extern "C" fn() ->
@@ -609,7 +603,6 @@ pub extern "C" fn cleanup_module() {
                 ::core::arch::global_asm!(#global_asm);
 
                 #[cfg(not(MODULE))]
-                #[doc(hidden)]
                 #[no_mangle]
                 pub extern "C" fn #ident_init() -> ::kernel::ffi::c_int {
                     // SAFETY: This function is inaccessible to the outside due to the double
@@ -619,7 +612,6 @@ pub extern "C" fn #ident_init() -> ::kernel::ffi::c_int {
                 }
 
                 #[cfg(not(MODULE))]
-                #[doc(hidden)]
                 #[no_mangle]
                 pub extern "C" fn #ident_exit() {
                     // SAFETY:
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* [PATCH v2 11/11] rust: kunit: use `pin_init::zeroed` instead of custom null value
  2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
                   ` (9 preceding siblings ...)
  2026-01-07 16:15 ` [PATCH v2 10/11] rust: macros: rearrange `#[doc(hidden)]` " Gary Guo
@ 2026-01-07 16:15 ` Gary Guo
  10 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 16:15 UTC (permalink / raw)
  To: Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Brendan Higgins, David Gow, Rae Moar
  Cc: rust-for-linux, Tamir Duberstein, linux-kselftest, kunit-dev,
	linux-kernel

From: Gary Guo <gary@garyguo.net>

The last null element can be created (constly) using `pin_init::zeroed`,
so prefer to use it instead of adding a custom way of building it.

Reviewed-by: Tamir Duberstein <tamird@gmail.com>
Reviewed-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
 rust/kernel/kunit.rs | 26 +-------------------------
 rust/macros/kunit.rs |  4 ++--
 2 files changed, 3 insertions(+), 27 deletions(-)

diff --git a/rust/kernel/kunit.rs b/rust/kernel/kunit.rs
index 79436509dd73d..034158cdaf06b 100644
--- a/rust/kernel/kunit.rs
+++ b/rust/kernel/kunit.rs
@@ -192,9 +192,6 @@ pub fn is_test_result_ok(t: impl TestResult) -> bool {
 }
 
 /// Represents an individual test case.
-///
-/// The [`kunit_unsafe_test_suite!`] macro expects a NULL-terminated list of valid test cases.
-/// Use [`kunit_case_null`] to generate such a delimiter.
 #[doc(hidden)]
 pub const fn kunit_case(
     name: &'static kernel::str::CStr,
@@ -215,27 +212,6 @@ pub const fn kunit_case(
     }
 }
 
-/// Represents the NULL test case delimiter.
-///
-/// The [`kunit_unsafe_test_suite!`] macro expects a NULL-terminated list of test cases. This
-/// function returns such a delimiter.
-#[doc(hidden)]
-pub const fn kunit_case_null() -> kernel::bindings::kunit_case {
-    kernel::bindings::kunit_case {
-        run_case: None,
-        name: core::ptr::null_mut(),
-        generate_params: None,
-        attr: kernel::bindings::kunit_attributes {
-            speed: kernel::bindings::kunit_speed_KUNIT_SPEED_NORMAL,
-        },
-        status: kernel::bindings::kunit_status_KUNIT_SUCCESS,
-        module_name: core::ptr::null_mut(),
-        log: core::ptr::null_mut(),
-        param_init: None,
-        param_exit: None,
-    }
-}
-
 /// Registers a KUnit test suite.
 ///
 /// # Safety
@@ -254,7 +230,7 @@ pub const fn kunit_case_null() -> kernel::bindings::kunit_case {
 ///
 /// static mut KUNIT_TEST_CASES: [kernel::bindings::kunit_case; 2] = [
 ///     kernel::kunit::kunit_case(kernel::c_str!("name"), test_fn),
-///     kernel::kunit::kunit_case_null(),
+///     pin_init::zeroed(),
 /// ];
 /// kernel::kunit_unsafe_test_suite!(suite_name, KUNIT_TEST_CASES);
 /// ```
diff --git a/rust/macros/kunit.rs b/rust/macros/kunit.rs
index afbc708cbdc50..cb29f350d5b2b 100644
--- a/rust/macros/kunit.rs
+++ b/rust/macros/kunit.rs
@@ -74,7 +74,7 @@ pub(crate) fn kunit_tests(test_suite: Ident, mut module: ItemMod) -> Result<Toke
     // static mut TEST_CASES: [::kernel::bindings::kunit_case; 3] = [
     //     ::kernel::kunit::kunit_case(::kernel::c_str!("foo"), kunit_rust_wrapper_foo),
     //     ::kernel::kunit::kunit_case(::kernel::c_str!("bar"), kunit_rust_wrapper_bar),
-    //     ::kernel::kunit::kunit_case_null(),
+    //     ::pin_init::zeroed(),
     // ];
     //
     // ::kernel::kunit_unsafe_test_suite!(kunit_test_suit_name, TEST_CASES);
@@ -159,7 +159,7 @@ macro_rules! assert_eq {
     processed_items.push(parse_quote! {
         static mut TEST_CASES: [::kernel::bindings::kunit_case; #num_tests_plus_1] = [
             #(#test_cases,)*
-            ::kernel::kunit::kunit_case_null(),
+            ::pin_init::zeroed(),
         ];
     });
     processed_items.push(parse_quote! {
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 01/11] rust: pin-init: internal: remove proc-macro[2] and quote workarounds
  2026-01-07 16:15 ` [PATCH v2 01/11] rust: pin-init: internal: remove proc-macro[2] and quote workarounds Gary Guo
@ 2026-01-07 16:40   ` Tamir Duberstein
  0 siblings, 0 replies; 23+ messages in thread
From: Tamir Duberstein @ 2026-01-07 16:40 UTC (permalink / raw)
  To: Gary Guo
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Fiona Behrens, rust-for-linux, Greg Kroah-Hartman,
	Christian Schrefl, linux-kernel

On Wed, Jan 7, 2026 at 11:29 AM Gary Guo <gary@kernel.org> wrote:
>
> From: Benno Lossin <lossin@kernel.org>
>
> The kernel only had the `proc-macro` library available, whereas the
> user-space version also used `proc-macro2` and `quote`. Now both are
> available to the kernel, making it possible to remove the workarounds.
>
> Clippy complains about unnecessary `.to_string()` as `proc-macro2`
> provides additional `PartialEq` impl, so they are removed.
>
> Signed-off-by: Benno Lossin <lossin@kernel.org>
> Co-developed-by: Gary Guo <gary@garyguo.net>
> Signed-off-by: Gary Guo <gary@garyguo.net>

Reviewed-by: Tamir Duberstein <tamird@gmail.com>

> ---
>  rust/Makefile                             | 16 ++++++++++------
>  rust/pin-init/internal/src/helpers.rs     |  7 ++-----
>  rust/pin-init/internal/src/lib.rs         | 16 ----------------
>  rust/pin-init/internal/src/pin_data.rs    | 18 ++++++------------
>  rust/pin-init/internal/src/pinned_drop.rs | 10 ++++------
>  rust/pin-init/internal/src/zeroable.rs    |  6 ++----
>  scripts/generate_rust_analyzer.py         |  2 +-
>  7 files changed, 25 insertions(+), 50 deletions(-)
>
> diff --git a/rust/Makefile b/rust/Makefile
> index 5d357dce1704d..96f0ac53ec0e8 100644
> --- a/rust/Makefile
> +++ b/rust/Makefile
> @@ -212,9 +212,10 @@ rustdoc-ffi: $(src)/ffi.rs rustdoc-core FORCE
>
>  rustdoc-pin_init_internal: private rustdoc_host = yes
>  rustdoc-pin_init_internal: private rustc_target_flags = --cfg kernel \
> -    --extern proc_macro --crate-type proc-macro
> +    --extern proc_macro --extern proc_macro2 --extern quote --extern syn \
> +    --crate-type proc-macro
>  rustdoc-pin_init_internal: $(src)/pin-init/internal/src/lib.rs \
> -    rustdoc-clean FORCE
> +    rustdoc-clean rustdoc-proc_macro2 rustdoc-quote rustdoc-syn FORCE
>         +$(call if_changed,rustdoc)
>
>  rustdoc-pin_init: private rustdoc_host = yes
> @@ -273,9 +274,10 @@ rusttestlib-macros: $(src)/macros/lib.rs \
>         +$(call if_changed,rustc_test_library)
>
>  rusttestlib-pin_init_internal: private rustc_target_flags = --cfg kernel \
> -    --extern proc_macro
> +    --extern proc_macro --extern proc_macro2 --extern quote --extern syn
>  rusttestlib-pin_init_internal: private rustc_test_library_proc = yes
> -rusttestlib-pin_init_internal: $(src)/pin-init/internal/src/lib.rs FORCE
> +rusttestlib-pin_init_internal: $(src)/pin-init/internal/src/lib.rs \
> +    rusttestlib-proc_macro2 rusttestlib-quote rusttestlib-syn FORCE
>         +$(call if_changed,rustc_test_library)
>
>  rusttestlib-pin_init: private rustc_target_flags = --extern pin_init_internal \
> @@ -547,8 +549,10 @@ $(obj)/$(libmacros_name): $(src)/macros/lib.rs $(obj)/libproc_macro2.rlib \
>      $(obj)/libquote.rlib $(obj)/libsyn.rlib FORCE
>         +$(call if_changed_dep,rustc_procmacro)
>
> -$(obj)/$(libpin_init_internal_name): private rustc_target_flags = --cfg kernel
> -$(obj)/$(libpin_init_internal_name): $(src)/pin-init/internal/src/lib.rs FORCE
> +$(obj)/$(libpin_init_internal_name): private rustc_target_flags = --cfg kernel \
> +    --extern proc_macro2 --extern quote --extern syn
> +$(obj)/$(libpin_init_internal_name): $(src)/pin-init/internal/src/lib.rs \
> +       $(obj)/libproc_macro2.rlib $(obj)/libquote.rlib $(obj)/libsyn.rlib FORCE
>         +$(call if_changed_dep,rustc_procmacro)
>
>  quiet_cmd_rustc_library = $(if $(skip_clippy),RUSTC,$(RUSTC_OR_CLIPPY_QUIET)) L $@
> diff --git a/rust/pin-init/internal/src/helpers.rs b/rust/pin-init/internal/src/helpers.rs
> index 236f989a50f2f..90f85eaa41231 100644
> --- a/rust/pin-init/internal/src/helpers.rs
> +++ b/rust/pin-init/internal/src/helpers.rs
> @@ -1,9 +1,6 @@
>  // SPDX-License-Identifier: Apache-2.0 OR MIT
>
> -#[cfg(not(kernel))]
> -use proc_macro2 as proc_macro;
> -
> -use proc_macro::{TokenStream, TokenTree};
> +use proc_macro2::{TokenStream, TokenTree};
>
>  /// Parsed generics.
>  ///
> @@ -101,7 +98,7 @@ pub(crate) fn parse_generics(input: TokenStream) -> (Generics, Vec<TokenTree>) {
>                      1 => {
>                          // Here depending on the token, it might be a generic variable name.
>                          match tt.clone() {
> -                            TokenTree::Ident(i) if at_start && i.to_string() == "const" => {
> +                            TokenTree::Ident(i) if at_start && i == "const" => {
>                                  let Some(name) = toks.next() else {
>                                      // Parsing error.
>                                      break;
> diff --git a/rust/pin-init/internal/src/lib.rs b/rust/pin-init/internal/src/lib.rs
> index 297b0129a5bfd..4c4dc639ce823 100644
> --- a/rust/pin-init/internal/src/lib.rs
> +++ b/rust/pin-init/internal/src/lib.rs
> @@ -7,27 +7,11 @@
>  //! `pin-init` proc macros.
>
>  #![cfg_attr(not(RUSTC_LINT_REASONS_IS_STABLE), feature(lint_reasons))]
> -// Allow `.into()` to convert
> -// - `proc_macro2::TokenStream` into `proc_macro::TokenStream` in the user-space version.
> -// - `proc_macro::TokenStream` into `proc_macro::TokenStream` in the kernel version.
> -//   Clippy warns on this conversion, but it's required by the user-space version.
> -//
> -// Remove once we have `proc_macro2` in the kernel.
> -#![allow(clippy::useless_conversion)]
>  // Documentation is done in the pin-init crate instead.
>  #![allow(missing_docs)]
>
>  use proc_macro::TokenStream;
>
> -#[cfg(kernel)]
> -#[path = "../../../macros/quote.rs"]
> -#[macro_use]
> -#[cfg_attr(not(kernel), rustfmt::skip)]
> -mod quote;
> -#[cfg(not(kernel))]
> -#[macro_use]
> -extern crate quote;
> -
>  mod helpers;
>  mod pin_data;
>  mod pinned_drop;
> diff --git a/rust/pin-init/internal/src/pin_data.rs b/rust/pin-init/internal/src/pin_data.rs
> index 87d4a7eb1d35e..86a53b37cc660 100644
> --- a/rust/pin-init/internal/src/pin_data.rs
> +++ b/rust/pin-init/internal/src/pin_data.rs
> @@ -1,10 +1,8 @@
>  // SPDX-License-Identifier: Apache-2.0 OR MIT
>
> -#[cfg(not(kernel))]
> -use proc_macro2 as proc_macro;
> -
>  use crate::helpers::{parse_generics, Generics};
> -use proc_macro::{Group, Punct, Spacing, TokenStream, TokenTree};
> +use proc_macro2::{Group, Punct, Spacing, TokenStream, TokenTree};
> +use quote::quote;
>
>  pub(crate) fn pin_data(args: TokenStream, input: TokenStream) -> TokenStream {
>      // This proc-macro only does some pre-parsing and then delegates the actual parsing to
> @@ -28,7 +26,7 @@ pub(crate) fn pin_data(args: TokenStream, input: TokenStream) -> TokenStream {
>      // The name of the struct with ty_generics.
>      let struct_name = rest
>          .iter()
> -        .skip_while(|tt| !matches!(tt, TokenTree::Ident(i) if i.to_string() == "struct"))
> +        .skip_while(|tt| !matches!(tt, TokenTree::Ident(i) if i == "struct"))
>          .nth(1)
>          .and_then(|tt| match tt {
>              TokenTree::Ident(_) => {
> @@ -65,7 +63,7 @@ pub(crate) fn pin_data(args: TokenStream, input: TokenStream) -> TokenStream {
>          .into_iter()
>          .flat_map(|tt| {
>              // We ignore top level `struct` tokens, since they would emit a compile error.
> -            if matches!(&tt, TokenTree::Ident(i) if i.to_string() == "struct") {
> +            if matches!(&tt, TokenTree::Ident(i) if i == "struct") {
>                  vec![tt]
>              } else {
>                  replace_self_and_deny_type_defs(&struct_name, tt, &mut errs)
> @@ -98,11 +96,7 @@ fn replace_self_and_deny_type_defs(
>  ) -> Vec<TokenTree> {
>      match tt {
>          TokenTree::Ident(ref i)
> -            if i.to_string() == "enum"
> -                || i.to_string() == "trait"
> -                || i.to_string() == "struct"
> -                || i.to_string() == "union"
> -                || i.to_string() == "impl" =>
> +            if i == "enum" || i == "trait" || i == "struct" || i == "union" || i == "impl" =>
>          {
>              errs.extend(
>                  format!(
> @@ -119,7 +113,7 @@ fn replace_self_and_deny_type_defs(
>              );
>              vec![tt]
>          }
> -        TokenTree::Ident(i) if i.to_string() == "Self" => struct_name.clone(),
> +        TokenTree::Ident(i) if i == "Self" => struct_name.clone(),
>          TokenTree::Literal(_) | TokenTree::Punct(_) | TokenTree::Ident(_) => vec![tt],
>          TokenTree::Group(g) => vec![TokenTree::Group(Group::new(
>              g.delimiter(),
> diff --git a/rust/pin-init/internal/src/pinned_drop.rs b/rust/pin-init/internal/src/pinned_drop.rs
> index c4ca7a70b726a..cf8cd1c429849 100644
> --- a/rust/pin-init/internal/src/pinned_drop.rs
> +++ b/rust/pin-init/internal/src/pinned_drop.rs
> @@ -1,15 +1,13 @@
>  // SPDX-License-Identifier: Apache-2.0 OR MIT
>
> -#[cfg(not(kernel))]
> -use proc_macro2 as proc_macro;
> -
> -use proc_macro::{TokenStream, TokenTree};
> +use proc_macro2::{TokenStream, TokenTree};
> +use quote::quote;
>
>  pub(crate) fn pinned_drop(_args: TokenStream, input: TokenStream) -> TokenStream {
>      let mut toks = input.into_iter().collect::<Vec<_>>();
>      assert!(!toks.is_empty());
>      // Ensure that we have an `impl` item.
> -    assert!(matches!(&toks[0], TokenTree::Ident(i) if i.to_string() == "impl"));
> +    assert!(matches!(&toks[0], TokenTree::Ident(i) if i == "impl"));
>      // Ensure that we are implementing `PinnedDrop`.
>      let mut nesting: usize = 0;
>      let mut pinned_drop_idx = None;
> @@ -27,7 +25,7 @@ pub(crate) fn pinned_drop(_args: TokenStream, input: TokenStream) -> TokenStream
>          if i >= 1 && nesting == 0 {
>              // Found the end of the generics, this should be `PinnedDrop`.
>              assert!(
> -                matches!(tt, TokenTree::Ident(i) if i.to_string() == "PinnedDrop"),
> +                matches!(tt, TokenTree::Ident(i) if i == "PinnedDrop"),
>                  "expected 'PinnedDrop', found: '{tt:?}'"
>              );
>              pinned_drop_idx = Some(i);
> diff --git a/rust/pin-init/internal/src/zeroable.rs b/rust/pin-init/internal/src/zeroable.rs
> index e0ed3998445cf..d8a5ef3883f4b 100644
> --- a/rust/pin-init/internal/src/zeroable.rs
> +++ b/rust/pin-init/internal/src/zeroable.rs
> @@ -1,10 +1,8 @@
>  // SPDX-License-Identifier: GPL-2.0
>
> -#[cfg(not(kernel))]
> -use proc_macro2 as proc_macro;
> -
>  use crate::helpers::{parse_generics, Generics};
> -use proc_macro::{TokenStream, TokenTree};
> +use proc_macro2::{TokenStream, TokenTree};
> +use quote::quote;
>
>  pub(crate) fn parse_zeroable_derive_input(
>      input: TokenStream,
> diff --git a/scripts/generate_rust_analyzer.py b/scripts/generate_rust_analyzer.py
> index 147d0cc940681..d31d938886589 100755
> --- a/scripts/generate_rust_analyzer.py
> +++ b/scripts/generate_rust_analyzer.py
> @@ -123,7 +123,7 @@ def generate_crates(srctree, objtree, sysroot_src, external_src, cfgs, core_edit
>      append_crate(
>          "pin_init_internal",
>          srctree / "rust" / "pin-init" / "internal" / "src" / "lib.rs",
> -        [],
> +        ["std", "proc_macro", "proc_macro2", "quote", "syn"],
>          cfg=["kernel"],
>          is_proc_macro=True,
>      )
>
> base-commit: 9ace4753a5202b02191d54e9fdf7f9e3d02b85eb
> --
> 2.51.2
>

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn`
  2026-01-07 16:15 ` [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn` Gary Guo
@ 2026-01-07 16:48   ` Tamir Duberstein
  2026-01-08 12:41     ` Gary Guo
  2026-01-11 17:03   ` Benno Lossin
  1 sibling, 1 reply; 23+ messages in thread
From: Tamir Duberstein @ 2026-01-07 16:48 UTC (permalink / raw)
  To: Gary Guo
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Igor Korotin, José Expósito, rust-for-linux,
	linux-kernel

On Wed, Jan 7, 2026 at 11:30 AM Gary Guo <gary@kernel.org> wrote:
>
> From: Gary Guo <gary@garyguo.net>
>
> `#[vtable]` is converted to use syn. This is more robust than the
> previous heuristic-based searching of defined methods and functions.
>
> When doing so, the trait and impl are split into two code paths as the
> types are distinct when parsed by `syn`.
>
> Signed-off-by: Gary Guo <gary@garyguo.net>

Reviewed-by: Tamir Duberstein <tamird@gmail.com>

> ---
>  rust/macros/lib.rs    |   9 ++-
>  rust/macros/vtable.rs | 163 ++++++++++++++++++++++--------------------
>  2 files changed, 93 insertions(+), 79 deletions(-)
>
> diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
> index 945982c21f703..9955c04dbaae3 100644
> --- a/rust/macros/lib.rs
> +++ b/rust/macros/lib.rs
> @@ -22,6 +22,8 @@
>
>  use proc_macro::TokenStream;
>
> +use syn::parse_macro_input;
> +
>  /// Declares a kernel module.
>  ///
>  /// The `type` argument should be a type which implements the [`Module`]
> @@ -204,8 +206,11 @@ pub fn module(ts: TokenStream) -> TokenStream {
>  ///
>  /// [`kernel::error::VTABLE_DEFAULT_ERROR`]: ../kernel/error/constant.VTABLE_DEFAULT_ERROR.html
>  #[proc_macro_attribute]
> -pub fn vtable(attr: TokenStream, ts: TokenStream) -> TokenStream {
> -    vtable::vtable(attr.into(), ts.into()).into()
> +pub fn vtable(attr: TokenStream, input: TokenStream) -> TokenStream {
> +    parse_macro_input!(attr as syn::parse::Nothing);
> +    vtable::vtable(parse_macro_input!(input))
> +        .unwrap_or_else(|e| e.into_compile_error())
> +        .into()
>  }
>
>  /// Export a function so that C code can call it via a header file.
> diff --git a/rust/macros/vtable.rs b/rust/macros/vtable.rs
> index a67d1cc81a2d3..a39bedb703973 100644
> --- a/rust/macros/vtable.rs
> +++ b/rust/macros/vtable.rs
> @@ -1,97 +1,106 @@
>  // SPDX-License-Identifier: GPL-2.0
>
>  use std::collections::HashSet;
> -use std::fmt::Write;
>
> -use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
> +use proc_macro2::{
> +    Ident,
> +    TokenStream, //
> +};
> +use quote::ToTokens;
> +use syn::{
> +    parse_quote,
> +    Error,
> +    ImplItem,
> +    Item,
> +    ItemImpl,
> +    ItemTrait,
> +    Result,
> +    TraitItem, //
> +};
>
> -pub(crate) fn vtable(_attr: TokenStream, ts: TokenStream) -> TokenStream {
> -    let mut tokens: Vec<_> = ts.into_iter().collect();
> +fn handle_trait(mut item: ItemTrait) -> Result<ItemTrait> {
> +    let mut functions = Vec::new();

Would be easier to propagate `#[cfg]`(as in your FIXME below) if you
collect the generated constants into this vector instead -- then you
can do all the code generation in the `for item in &item.items` loop
where you have all the attributes.

> +    for item in &item.items {
> +        if let TraitItem::Fn(fn_item) = item {
> +            functions.push(fn_item.sig.ident.clone());
> +        }
> +    }
> +
> +    item.items.push(parse_quote! {
> +         /// A marker to prevent implementors from forgetting to use [`#[vtable]`](vtable)
> +         /// attribute when implementing this trait.
> +         const USE_VTABLE_ATTR: ();
> +    });
>
> -    // Scan for the `trait` or `impl` keyword.
> -    let is_trait = tokens
> -        .iter()
> -        .find_map(|token| match token {
> -            TokenTree::Ident(ident) => match ident.to_string().as_str() {
> -                "trait" => Some(true),
> -                "impl" => Some(false),
> -                _ => None,
> -            },
> -            _ => None,
> -        })
> -        .expect("#[vtable] attribute should only be applied to trait or impl block");
> +    let mut consts = HashSet::new();
> +    for name in functions {
> +        let gen_const_name = Ident::new(
> +            &format!("HAS_{}", name.to_string().to_uppercase()),
> +            name.span(),
> +        );
> +        // Skip if it's declared already -- this can happen if `#[cfg]` is used to selectively
> +        // define functions.
> +        // FIXME: `#[cfg]` should be copied and propagated to the generated consts.
> +        if consts.contains(&gen_const_name) {
> +            continue;
> +        }
> +        // We don't know on the implementation-site whether a method is required or provided
> +        // so we have to generate a const for all methods.
> +        let comment = format!("Indicates if the `{name}` method is overridden by the implementor.");

We're already quasi-quoting below, does putting the comment there work?

> +        item.items.push(parse_quote! {
> +            #[doc = #comment]
> +            const #gen_const_name: bool = false;
> +        });
> +        consts.insert(gen_const_name);
> +    }
>
> -    // Retrieve the main body. The main body should be the last token tree.
> -    let body = match tokens.pop() {
> -        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => group,
> -        _ => panic!("cannot locate main body of trait or impl block"),
> -    };
> +    Ok(item)
> +}
>
> -    let mut body_it = body.stream().into_iter();
> +fn handle_impl(mut item: ItemImpl) -> Result<ItemImpl> {
>      let mut functions = Vec::new();
>      let mut consts = HashSet::new();
> -    while let Some(token) = body_it.next() {
> -        match token {
> -            TokenTree::Ident(ident) if ident == "fn" => {
> -                let fn_name = match body_it.next() {
> -                    Some(TokenTree::Ident(ident)) => ident.to_string(),
> -                    // Possibly we've encountered a fn pointer type instead.
> -                    _ => continue,
> -                };
> -                functions.push(fn_name);
> +    for item in &item.items {
> +        match item {
> +            ImplItem::Fn(fn_item) => {
> +                functions.push(fn_item.sig.ident.clone());
>              }
> -            TokenTree::Ident(ident) if ident == "const" => {
> -                let const_name = match body_it.next() {
> -                    Some(TokenTree::Ident(ident)) => ident.to_string(),
> -                    // Possibly we've encountered an inline const block instead.
> -                    _ => continue,
> -                };
> -                consts.insert(const_name);
> +            ImplItem::Const(const_item) => {
> +                consts.insert(const_item.ident.clone());
>              }
> -            _ => (),
> +            _ => {}
>          }
>      }
>
> -    let mut const_items;
> -    if is_trait {
> -        const_items = "
> -                /// A marker to prevent implementors from forgetting to use [`#[vtable]`](vtable)
> -                /// attribute when implementing this trait.
> -                const USE_VTABLE_ATTR: ();
> -        "
> -        .to_owned();
> +    item.items.push(parse_quote! {
> +         const USE_VTABLE_ATTR: () = ();
> +    });
>
> -        for f in functions {
> -            let gen_const_name = format!("HAS_{}", f.to_uppercase());
> -            // Skip if it's declared already -- this allows user override.
> -            if consts.contains(&gen_const_name) {
> -                continue;
> -            }
> -            // We don't know on the implementation-site whether a method is required or provided
> -            // so we have to generate a const for all methods.
> -            write!(
> -                const_items,
> -                "/// Indicates if the `{f}` method is overridden by the implementor.
> -                const {gen_const_name}: bool = false;",
> -            )
> -            .unwrap();
> -            consts.insert(gen_const_name);
> -        }
> -    } else {
> -        const_items = "const USE_VTABLE_ATTR: () = ();".to_owned();
> -
> -        for f in functions {
> -            let gen_const_name = format!("HAS_{}", f.to_uppercase());
> -            if consts.contains(&gen_const_name) {
> -                continue;
> -            }
> -            write!(const_items, "const {gen_const_name}: bool = true;").unwrap();
> +    for name in functions {
> +        let gen_const_name = Ident::new(
> +            &format!("HAS_{}", name.to_string().to_uppercase()),
> +            name.span(),
> +        );
> +        // Skip if it's declared already -- this allows user override.
> +        if consts.contains(&gen_const_name) {
> +            continue;
>          }
> +        item.items.push(parse_quote! {
> +            const #gen_const_name: bool = true;
> +        });
> +        consts.insert(gen_const_name);
>      }
>
> -    let new_body = vec![const_items.parse().unwrap(), body.stream()]
> -        .into_iter()
> -        .collect();
> -    tokens.push(TokenTree::Group(Group::new(Delimiter::Brace, new_body)));
> -    tokens.into_iter().collect()
> +    Ok(item)
> +}
> +
> +pub(crate) fn vtable(input: Item) -> Result<TokenStream> {
> +    match input {
> +        Item::Trait(item) => Ok(handle_trait(item)?.into_token_stream()),
> +        Item::Impl(item) => Ok(handle_impl(item)?.into_token_stream()),
> +        _ => Err(Error::new_spanned(
> +            input,
> +            "`#[vtable]` attribute should only be applied to trait or impl block",
> +        ))?,
> +    }
>  }
> --
> 2.51.2
>

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 04/11] rust: macros: use `syn` to parse `module!` macro
  2026-01-07 16:15 ` [PATCH v2 04/11] rust: macros: use `syn` to parse `module!` macro Gary Guo
@ 2026-01-07 17:11   ` Tamir Duberstein
  0 siblings, 0 replies; 23+ messages in thread
From: Tamir Duberstein @ 2026-01-07 17:11 UTC (permalink / raw)
  To: Gary Guo
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, José Expósito, rust-for-linux,
	Patrick Miller, linux-kernel, linux-modules

On Wed, Jan 7, 2026 at 11:30 AM Gary Guo <gary@kernel.org> wrote:
>
> From: Gary Guo <gary@garyguo.net>
>
> With `syn` being available in the kernel, use it to parse the complex
> custom `module!` macro to replace existing helpers. Only parsing is
> changed in this commit, the code generation is untouched.
>
> This has the benefit of better error message when the macro is used
> incorrectly, as it can point to a concrete span on what's going wrong.
>
> For example, if a field is specified twice, previously it reads:
>
>     error: proc macro panicked
>       --> samples/rust/rust_minimal.rs:7:1
>        |
>     7  | / module! {
>     8  | |     type: RustMinimal,
>     9  | |     name: "rust_minimal",
>     10 | |     author: "Rust for Linux Contributors",
>     11 | |     description: "Rust minimal sample",
>     12 | |     license: "GPL",
>     13 | |     license: "GPL",
>     14 | | }
>        | |_^
>        |
>        = help: message: Duplicated key "license". Keys can only be specified once.
>
> now it reads:
>
>     error: duplicated key "license". Keys can only be specified once.
>       --> samples/rust/rust_minimal.rs:13:5
>        |
>     13 |     license: "GPL",
>        |     ^^^^^^^
>
> Signed-off-by: Gary Guo <gary@garyguo.net>

Reviewed-by: Tamir Duberstein <tamird@gmail.com>

> ---
>  rust/macros/helpers.rs | 109 ++++--------
>  rust/macros/lib.rs     |   6 +-
>  rust/macros/module.rs  | 389 +++++++++++++++++++++++++----------------
>  3 files changed, 277 insertions(+), 227 deletions(-)
>
> diff --git a/rust/macros/helpers.rs b/rust/macros/helpers.rs
> index 13fafaba12261..fa66ef6eb0f3d 100644
> --- a/rust/macros/helpers.rs
> +++ b/rust/macros/helpers.rs
> @@ -1,53 +1,21 @@
>  // SPDX-License-Identifier: GPL-2.0
>
> -use proc_macro2::{token_stream, Group, Ident, TokenStream, TokenTree};
> -
> -pub(crate) fn try_ident(it: &mut token_stream::IntoIter) -> Option<String> {
> -    if let Some(TokenTree::Ident(ident)) = it.next() {
> -        Some(ident.to_string())
> -    } else {
> -        None
> -    }
> -}
> -
> -pub(crate) fn try_sign(it: &mut token_stream::IntoIter) -> Option<char> {
> -    let peek = it.clone().next();
> -    match peek {
> -        Some(TokenTree::Punct(punct)) if punct.as_char() == '-' => {
> -            let _ = it.next();
> -            Some(punct.as_char())
> -        }
> -        _ => None,
> -    }
> -}
> -
> -pub(crate) fn try_literal(it: &mut token_stream::IntoIter) -> Option<String> {
> -    if let Some(TokenTree::Literal(literal)) = it.next() {
> -        Some(literal.to_string())
> -    } else {
> -        None
> -    }
> -}
> -
> -pub(crate) fn try_string(it: &mut token_stream::IntoIter) -> Option<String> {
> -    try_literal(it).and_then(|string| {
> -        if string.starts_with('\"') && string.ends_with('\"') {
> -            let content = &string[1..string.len() - 1];
> -            if content.contains('\\') {
> -                panic!("Escape sequences in string literals not yet handled");
> -            }
> -            Some(content.to_string())
> -        } else if string.starts_with("r\"") {
> -            panic!("Raw string literals are not yet handled");
> -        } else {
> -            None
> -        }
> -    })
> -}
> -
> -pub(crate) fn expect_ident(it: &mut token_stream::IntoIter) -> String {
> -    try_ident(it).expect("Expected Ident")
> -}
> +use proc_macro2::{
> +    token_stream,
> +    Ident,
> +    TokenStream,
> +    TokenTree, //
> +};
> +use quote::ToTokens;
> +use syn::{
> +    parse::{
> +        Parse,
> +        ParseStream, //
> +    },
> +    Error,
> +    LitStr,
> +    Result, //
> +};
>
>  pub(crate) fn expect_punct(it: &mut token_stream::IntoIter) -> char {
>      if let TokenTree::Punct(punct) = it.next().expect("Reached end of token stream for Punct") {
> @@ -57,27 +25,28 @@ pub(crate) fn expect_punct(it: &mut token_stream::IntoIter) -> char {
>      }
>  }
>
> -pub(crate) fn expect_string(it: &mut token_stream::IntoIter) -> String {
> -    try_string(it).expect("Expected string")
> -}
> +/// A string literal that is required to have ASCII value only.
> +pub(crate) struct AsciiLitStr(LitStr);
>
> -pub(crate) fn expect_string_ascii(it: &mut token_stream::IntoIter) -> String {
> -    let string = try_string(it).expect("Expected string");
> -    assert!(string.is_ascii(), "Expected ASCII string");
> -    string
> +impl Parse for AsciiLitStr {
> +    fn parse(input: ParseStream<'_>) -> Result<Self> {
> +        let s: LitStr = input.parse()?;
> +        if !s.value().is_ascii() {
> +            return Err(Error::new_spanned(s, "expected ASCII-only string literal"));
> +        }
> +        Ok(Self(s))
> +    }
>  }
>
> -pub(crate) fn expect_group(it: &mut token_stream::IntoIter) -> Group {
> -    if let TokenTree::Group(group) = it.next().expect("Reached end of token stream for Group") {
> -        group
> -    } else {
> -        panic!("Expected Group");
> +impl ToTokens for AsciiLitStr {
> +    fn to_tokens(&self, ts: &mut TokenStream) {
> +        self.0.to_tokens(ts);
>      }
>  }
>
> -pub(crate) fn expect_end(it: &mut token_stream::IntoIter) {
> -    if it.next().is_some() {
> -        panic!("Expected end");
> +impl AsciiLitStr {
> +    pub(crate) fn value(&self) -> String {
> +        self.0.value()
>      }
>  }
>
> @@ -114,17 +83,3 @@ pub(crate) fn file() -> String {
>          proc_macro::Span::call_site().file()
>      }
>  }
> -
> -/// Parse a token stream of the form `expected_name: "value",` and return the
> -/// string in the position of "value".
> -///
> -/// # Panics
> -///
> -/// - On parse error.
> -pub(crate) fn expect_string_field(it: &mut token_stream::IntoIter, expected_name: &str) -> String {
> -    assert_eq!(expect_ident(it), expected_name);
> -    assert_eq!(expect_punct(it), ':');
> -    let string = expect_string(it);
> -    assert_eq!(expect_punct(it), ',');
> -    string
> -}
> diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
> index 9955c04dbaae3..c5347127a3a51 100644
> --- a/rust/macros/lib.rs
> +++ b/rust/macros/lib.rs
> @@ -131,8 +131,10 @@
>  ///   - `firmware`: array of ASCII string literals of the firmware files of
>  ///     the kernel module.
>  #[proc_macro]
> -pub fn module(ts: TokenStream) -> TokenStream {
> -    module::module(ts.into()).into()
> +pub fn module(input: TokenStream) -> TokenStream {
> +    module::module(parse_macro_input!(input))
> +        .unwrap_or_else(|e| e.into_compile_error())
> +        .into()
>  }
>
>  /// Declares or implements a vtable trait.
> diff --git a/rust/macros/module.rs b/rust/macros/module.rs
> index b855a2b586e18..6ad7b411ccde4 100644
> --- a/rust/macros/module.rs
> +++ b/rust/macros/module.rs
> @@ -2,28 +2,30 @@
>
>  use std::fmt::Write;
>
> -use proc_macro2::{token_stream, Delimiter, Literal, TokenStream, TokenTree};
> +use proc_macro2::{
> +    Literal,
> +    TokenStream, //
> +};
> +use quote::ToTokens;
> +use syn::{
> +    braced,
> +    bracketed,
> +    ext::IdentExt,
> +    parse::{
> +        Parse,
> +        ParseStream, //
> +    },
> +    punctuated::Punctuated,
> +    Error,
> +    Expr,
> +    Ident,
> +    LitStr,
> +    Result,
> +    Token, //
> +};
>
>  use crate::helpers::*;
>
> -fn expect_string_array(it: &mut token_stream::IntoIter) -> Vec<String> {
> -    let group = expect_group(it);
> -    assert_eq!(group.delimiter(), Delimiter::Bracket);
> -    let mut values = Vec::new();
> -    let mut it = group.stream().into_iter();
> -
> -    while let Some(val) = try_string(&mut it) {
> -        assert!(val.is_ascii(), "Expected ASCII string");
> -        values.push(val);
> -        match it.next() {
> -            Some(TokenTree::Punct(punct)) => assert_eq!(punct.as_char(), ','),
> -            None => break,
> -            _ => panic!("Expected ',' or end of array"),
> -        }
> -    }
> -    values
> -}
> -
>  struct ModInfoBuilder<'a> {
>      module: &'a str,
>      counter: usize,
> @@ -113,12 +115,16 @@ fn emit_params(&mut self, info: &ModuleInfo) {
>          };
>
>          for param in params {
> -            let ops = param_ops_path(&param.ptype);
> +            let param_name = param.name.to_string();
> +            let param_type = param.ptype.to_string();
> +            let param_default = param.default.to_token_stream().to_string();
> +
> +            let ops = param_ops_path(&param_type);
>
>              // Note: The spelling of these fields is dictated by the user space
>              // tool `modinfo`.
> -            self.emit_param("parmtype", &param.name, &param.ptype);
> -            self.emit_param("parm", &param.name, &param.description);
> +            self.emit_param("parmtype", &param_name, &param_type);
> +            self.emit_param("parm", &param_name, &param.description.value());
>
>              write!(
>                  self.param_buffer,
> @@ -160,10 +166,7 @@ fn emit_params(&mut self, info: &ModuleInfo) {
>                          );
>                  }};
>                  ",
> -                module_name = info.name,
> -                param_type = param.ptype,
> -                param_default = param.default,
> -                param_name = param.name,
> +                module_name = info.name.value(),
>                  ops = ops,
>              )
>              .unwrap();
> @@ -187,127 +190,82 @@ fn param_ops_path(param_type: &str) -> &'static str {
>      }
>  }
>
> -fn expect_param_default(param_it: &mut token_stream::IntoIter) -> String {
> -    assert_eq!(expect_ident(param_it), "default");
> -    assert_eq!(expect_punct(param_it), ':');
> -    let sign = try_sign(param_it);
> -    let default = try_literal(param_it).expect("Expected default param value");
> -    assert_eq!(expect_punct(param_it), ',');
> -    let mut value = sign.map(String::from).unwrap_or_default();
> -    value.push_str(&default);
> -    value
> -}
> -
> -#[derive(Debug, Default)]
> -struct ModuleInfo {
> -    type_: String,
> -    license: String,
> -    name: String,
> -    authors: Option<Vec<String>>,
> -    description: Option<String>,
> -    alias: Option<Vec<String>>,
> -    firmware: Option<Vec<String>>,
> -    imports_ns: Option<Vec<String>>,
> -    params: Option<Vec<Parameter>>,
> -}
> -
> -#[derive(Debug)]
> -struct Parameter {
> -    name: String,
> -    ptype: String,
> -    default: String,
> -    description: String,
> -}
> -
> -fn expect_params(it: &mut token_stream::IntoIter) -> Vec<Parameter> {
> -    let params = expect_group(it);
> -    assert_eq!(params.delimiter(), Delimiter::Brace);
> -    let mut it = params.stream().into_iter();
> -    let mut parsed = Vec::new();
> -
> -    loop {
> -        let param_name = match it.next() {
> -            Some(TokenTree::Ident(ident)) => ident.to_string(),
> -            Some(_) => panic!("Expected Ident or end"),
> -            None => break,
> -        };
> -
> -        assert_eq!(expect_punct(&mut it), ':');
> -        let param_type = expect_ident(&mut it);
> -        let group = expect_group(&mut it);
> -        assert_eq!(group.delimiter(), Delimiter::Brace);
> -        assert_eq!(expect_punct(&mut it), ',');
> -
> -        let mut param_it = group.stream().into_iter();
> -        let param_default = expect_param_default(&mut param_it);
> -        let param_description = expect_string_field(&mut param_it, "description");
> -        expect_end(&mut param_it);
> -
> -        parsed.push(Parameter {
> -            name: param_name,
> -            ptype: param_type,
> -            default: param_default,
> -            description: param_description,
> -        })
> -    }
> -
> -    parsed
> -}
> -
> -impl ModuleInfo {
> -    fn parse(it: &mut token_stream::IntoIter) -> Self {
> -        let mut info = ModuleInfo::default();
> -
> -        const EXPECTED_KEYS: &[&str] = &[
> -            "type",
> -            "name",
> -            "authors",
> -            "description",
> -            "license",
> -            "alias",
> -            "firmware",
> -            "imports_ns",
> -            "params",
> -        ];
> -        const REQUIRED_KEYS: &[&str] = &["type", "name", "license"];
> +/// Parse fields that are required to use a specific order.
> +///
> +/// As fields must follow a specific order, we *could* just parse fields one by one by peeking.
> +/// However the error message generated when implementing that way is not very friendly.
> +///
> +/// So instead we parse fields in an arbitrary order, but only enforce the ordering after parsing,
> +/// and if the wrong order is used, the proper order is communicated to the user with error message.
> +///
> +/// Usage looks like this:
> +/// ```ignore
> +/// parse_ordered_fields! {
> +///     from input;
> +///
> +///     // This will extract "foo: <field>" into a variable named "foo".
> +///     // The variable will have type `Option<_>`.
> +///     foo => <expression that parses the field>,
> +///
> +///     // If you need the variable name to be different than the key name.
> +///     // This extracts "baz: <field>" into a variable named "bar".
> +///     // You might want this if "baz" is a keyword.
> +///     baz as bar => <expression that parse the field>,
> +///
> +///     // You can mark a key as required, and the variable will no longer be `Option`.
> +///     // foobar will be of type `Expr` instead of `Option<Expr>`.
> +///     foobar [required] => input.parse::<Expr>()?,
> +/// }
> +/// ```

It's a shame that this macro bails on the first failure rather than
accumulating them all.

> +macro_rules! parse_ordered_fields {
> +    (@gen
> +        [$input:expr]
> +        [$([$name:ident; $key:ident; $parser:expr])*]
> +        [$([$req_name:ident; $req_key:ident])*]
> +    ) => {
> +        $(let mut $name = None;)*
> +
> +        const EXPECTED_KEYS: &[&str] = &[$(stringify!($key),)*];
> +        const REQUIRED_KEYS: &[&str] = &[$(stringify!($req_key),)*];
> +
> +        let span = $input.span();
>          let mut seen_keys = Vec::new();
>
>          loop {
> -            let key = match it.next() {
> -                Some(TokenTree::Ident(ident)) => ident.to_string(),
> -                Some(_) => panic!("Expected Ident or end"),
> -                None => break,
> -            };
> +            if $input.is_empty() {
> +                break;
> +            }

`while !$input.is_empty()`?

> +
> +            let key = $input.call(Ident::parse_any)?;
>
>              if seen_keys.contains(&key) {
> -                panic!("Duplicated key \"{key}\". Keys can only be specified once.");
> +                Err(Error::new_spanned(
> +                    &key,
> +                    format!(r#"duplicated key "{key}". Keys can only be specified once."#),
> +                ))?
>              }
>
> -            assert_eq!(expect_punct(it), ':');
> -
> -            match key.as_str() {
> -                "type" => info.type_ = expect_ident(it),
> -                "name" => info.name = expect_string_ascii(it),
> -                "authors" => info.authors = Some(expect_string_array(it)),
> -                "description" => info.description = Some(expect_string(it)),
> -                "license" => info.license = expect_string_ascii(it),
> -                "alias" => info.alias = Some(expect_string_array(it)),
> -                "firmware" => info.firmware = Some(expect_string_array(it)),
> -                "imports_ns" => info.imports_ns = Some(expect_string_array(it)),
> -                "params" => info.params = Some(expect_params(it)),
> -                _ => panic!("Unknown key \"{key}\". Valid keys are: {EXPECTED_KEYS:?}."),
> +            $input.parse::<Token![:]>()?;
> +
> +            match &*key.to_string() {
> +                $(
> +                    stringify!($key) => $name = Some($parser),
> +                )*
> +                _ => {
> +                    Err(Error::new_spanned(
> +                        &key,
> +                        format!(r#"unknown key "{key}". Valid keys are: {EXPECTED_KEYS:?}."#),
> +                    ))?
> +                }
>              }
>
> -            assert_eq!(expect_punct(it), ',');
> -
> +            $input.parse::<Token![,]>()?;
>              seen_keys.push(key);
>          }
>
> -        expect_end(it);
> -
>          for key in REQUIRED_KEYS {
>              if !seen_keys.iter().any(|e| e == key) {
> -                panic!("Missing required key \"{key}\".");
> +                Err(Error::new(span, format!(r#"missing required key "{key}""#)))?
>              }
>          }
>
> @@ -319,43 +277,178 @@ fn parse(it: &mut token_stream::IntoIter) -> Self {
>          }
>
>          if seen_keys != ordered_keys {
> -            panic!("Keys are not ordered as expected. Order them like: {ordered_keys:?}.");
> +            Err(Error::new(
> +                span,
> +                format!(r#"keys are not ordered as expected. Order them like: {ordered_keys:?}."#),
> +            ))?
> +        }
> +
> +        $(let $req_name = $req_name.expect("required field");)*
> +    };
> +
> +    // Handle required fields.
> +    (@gen
> +        [$input:expr] [$($tok:tt)*] [$($req:tt)*]
> +        $key:ident as $name:ident [required] => $parser:expr,
> +        $($rest:tt)*
> +    ) => {
> +        parse_ordered_fields!(
> +            @gen [$input] [$($tok)* [$name; $key; $parser]] [$($req)* [$name; $key]] $($rest)*
> +        )
> +    };
> +    (@gen
> +        [$input:expr] [$($tok:tt)*] [$($req:tt)*]
> +        $name:ident [required] => $parser:expr,
> +        $($rest:tt)*
> +    ) => {
> +        parse_ordered_fields!(
> +            @gen [$input] [$($tok)* [$name; $name; $parser]] [$($req)* [$name; $name]] $($rest)*
> +        )
> +    };
> +
> +    // Handle optional fields.
> +    (@gen
> +        [$input:expr] [$($tok:tt)*] [$($req:tt)*]
> +        $key:ident as $name:ident => $parser:expr,
> +        $($rest:tt)*
> +    ) => {
> +        parse_ordered_fields!(
> +            @gen [$input] [$($tok)* [$name; $key; $parser]] [$($req)*] $($rest)*
> +        )
> +    };
> +    (@gen
> +        [$input:expr] [$($tok:tt)*] [$($req:tt)*]
> +        $name:ident => $parser:expr,
> +        $($rest:tt)*
> +    ) => {
> +        parse_ordered_fields!(
> +            @gen [$input] [$($tok)* [$name; $name; $parser]] [$($req)*] $($rest)*
> +        )
> +    };

It would be nice to avoid the combinatorial explosion here, though I'm
not immediately sure how.

> +
> +    (from $input:expr; $($tok:tt)*) => {
> +        parse_ordered_fields!(@gen [$input] [] [] $($tok)*)
> +    }
> +}
> +
> +struct Parameter {
> +    name: Ident,
> +    ptype: Ident,
> +    default: Expr,
> +    description: LitStr,
> +}
> +
> +impl Parse for Parameter {
> +    fn parse(input: ParseStream<'_>) -> Result<Self> {
> +        let name = input.parse()?;
> +        input.parse::<Token![:]>()?;
> +        let ptype = input.parse()?;
> +
> +        let fields;
> +        braced!(fields in input);
> +
> +        parse_ordered_fields! {
> +            from fields;
> +            default [required] => fields.parse()?,
> +            description [required] => fields.parse()?,
>          }
>
> -        info
> +        Ok(Self {
> +            name,
> +            ptype,
> +            default,
> +            description,
> +        })
>      }
>  }
>
> -pub(crate) fn module(ts: TokenStream) -> TokenStream {
> -    let mut it = ts.into_iter();
> +pub(crate) struct ModuleInfo {
> +    type_: Ident,
> +    license: AsciiLitStr,
> +    name: AsciiLitStr,
> +    authors: Option<Punctuated<AsciiLitStr, Token![,]>>,
> +    description: Option<LitStr>,
> +    alias: Option<Punctuated<AsciiLitStr, Token![,]>>,
> +    firmware: Option<Punctuated<AsciiLitStr, Token![,]>>,
> +    imports_ns: Option<Punctuated<AsciiLitStr, Token![,]>>,
> +    params: Option<Punctuated<Parameter, Token![,]>>,
> +}
>
> -    let info = ModuleInfo::parse(&mut it);
> +impl Parse for ModuleInfo {
> +    fn parse(input: ParseStream<'_>) -> Result<Self> {
> +        parse_ordered_fields!(
> +            from input;
> +            type as type_ [required] => input.parse()?,
> +            name [required] => input.parse()?,
> +            authors => {
> +                let list;
> +                bracketed!(list in input);
> +                Punctuated::parse_terminated(&list)?
> +            },
> +            description => input.parse()?,
> +            license [required] => input.parse()?,
> +            alias => {
> +                let list;
> +                bracketed!(list in input);
> +                Punctuated::parse_terminated(&list)?
> +            },
> +            firmware => {
> +                let list;
> +                bracketed!(list in input);
> +                Punctuated::parse_terminated(&list)?
> +            },
> +            imports_ns => {
> +                let list;
> +                bracketed!(list in input);
> +                Punctuated::parse_terminated(&list)?
> +            },
> +            params => {
> +                let list;
> +                braced!(list in input);
> +                Punctuated::parse_terminated(&list)?
> +            },
> +        );
> +
> +        Ok(ModuleInfo {
> +            type_,
> +            license,
> +            name,
> +            authors,
> +            description,
> +            alias,
> +            firmware,
> +            imports_ns,
> +            params,
> +        })
> +    }
> +}
>
> +pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
>      // Rust does not allow hyphens in identifiers, use underscore instead.
> -    let ident = info.name.replace('-', "_");
> +    let ident = info.name.value().replace('-', "_");
>      let mut modinfo = ModInfoBuilder::new(ident.as_ref());
>      if let Some(authors) = &info.authors {
>          for author in authors {
> -            modinfo.emit("author", author);
> +            modinfo.emit("author", &author.value());
>          }
>      }
>      if let Some(description) = &info.description {
> -        modinfo.emit("description", description);
> +        modinfo.emit("description", &description.value());
>      }
> -    modinfo.emit("license", &info.license);
> +    modinfo.emit("license", &info.license.value());
>      if let Some(aliases) = &info.alias {
>          for alias in aliases {
> -            modinfo.emit("alias", alias);
> +            modinfo.emit("alias", &alias.value());
>          }
>      }
>      if let Some(firmware) = &info.firmware {
>          for fw in firmware {
> -            modinfo.emit("firmware", fw);
> +            modinfo.emit("firmware", &fw.value());
>          }
>      }
>      if let Some(imports) = &info.imports_ns {
>          for ns in imports {
> -            modinfo.emit("import_ns", ns);
> +            modinfo.emit("import_ns", &ns.value());
>          }
>      }
>
> @@ -366,7 +459,7 @@ pub(crate) fn module(ts: TokenStream) -> TokenStream {
>
>      modinfo.emit_params(&info);
>
> -    format!(
> +    Ok(format!(
>          "
>              /// The module name.
>              ///
> @@ -536,12 +629,12 @@ mod module_parameters {{
>              }}
>          ",
>          type_ = info.type_,
> -        name = info.name,
> +        name = info.name.value(),
>          ident = ident,
>          modinfo = modinfo.buffer,
>          params = modinfo.param_buffer,
>          initcall_section = ".initcall6.init"
>      )
>      .parse()
> -    .expect("Error parsing formatted string into token stream.")
> +    .expect("Error parsing formatted string into token stream."))
>  }
> --
> 2.51.2
>

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 05/11] rust: macros: use `quote!` for `module!` macro
  2026-01-07 16:15 ` [PATCH v2 05/11] rust: macros: use `quote!` for " Gary Guo
@ 2026-01-07 17:19   ` Tamir Duberstein
  2026-01-07 17:54     ` Gary Guo
  0 siblings, 1 reply; 23+ messages in thread
From: Tamir Duberstein @ 2026-01-07 17:19 UTC (permalink / raw)
  To: Gary Guo
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, rust-for-linux, linux-modules, linux-kernel

On Wed, Jan 7, 2026 at 11:59 AM Gary Guo <gary@kernel.org> wrote:
>
> From: Gary Guo <gary@garyguo.net>
>
> This has no behavioural change, but is good for maintainability. With
> `quote!`, we're no longer using string templates, so we don't need to
> quote " and {} inside the template anymore. Further more, editors can
> now highlight the code template.
>
> This also improves the robustness as it eliminates the need for string
> quoting and escaping.
>
> Co-developed-by: Benno Lossin <lossin@kernel.org>
> Signed-off-by: Benno Lossin <lossin@kernel.org>
> Signed-off-by: Gary Guo <gary@garyguo.net>

Are there significant changes here? You didn't pick up my tag.

> ---
>  rust/macros/module.rs | 558 ++++++++++++++++++++++--------------------
>  1 file changed, 295 insertions(+), 263 deletions(-)
>
> diff --git a/rust/macros/module.rs b/rust/macros/module.rs
> index 6ad7b411ccde4..ba345d672839e 100644
> --- a/rust/macros/module.rs
> +++ b/rust/macros/module.rs
> @@ -1,12 +1,15 @@
>  // SPDX-License-Identifier: GPL-2.0
>
> -use std::fmt::Write;
> +use std::ffi::CString;
>
>  use proc_macro2::{
>      Literal,
>      TokenStream, //
>  };
> -use quote::ToTokens;
> +use quote::{
> +    format_ident,
> +    quote, //
> +};
>  use syn::{
>      braced,
>      bracketed,
> @@ -15,11 +18,13 @@
>          Parse,
>          ParseStream, //
>      },
> +    parse_quote,
>      punctuated::Punctuated,
>      Error,
>      Expr,
>      Ident,
>      LitStr,
> +    Path,
>      Result,
>      Token, //
>  };
> @@ -29,8 +34,8 @@
>  struct ModInfoBuilder<'a> {
>      module: &'a str,
>      counter: usize,
> -    buffer: String,
> -    param_buffer: String,
> +    ts: TokenStream,
> +    param_ts: TokenStream,
>  }
>
>  impl<'a> ModInfoBuilder<'a> {
> @@ -38,8 +43,8 @@ fn new(module: &'a str) -> Self {
>          ModInfoBuilder {
>              module,
>              counter: 0,
> -            buffer: String::new(),
> -            param_buffer: String::new(),
> +            ts: TokenStream::new(),
> +            param_ts: TokenStream::new(),
>          }
>      }
>
> @@ -56,33 +61,32 @@ fn emit_base(&mut self, field: &str, content: &str, builtin: bool, param: bool)
>              // Loadable modules' modinfo strings go as-is.
>              format!("{field}={content}\0")
>          };
> -
> -        let buffer = if param {
> -            &mut self.param_buffer
> +        let length = string.len();
> +        let string = Literal::byte_string(string.as_bytes());
> +        let cfg = if builtin {
> +            quote!(#[cfg(not(MODULE))])
>          } else {
> -            &mut self.buffer
> +            quote!(#[cfg(MODULE)])
>          };
>
> -        write!(
> -            buffer,
> -            "
> -                {cfg}
> -                #[doc(hidden)]
> -                #[cfg_attr(not(target_os = \"macos\"), link_section = \".modinfo\")]
> -                #[used(compiler)]
> -                pub static __{module}_{counter}: [u8; {length}] = *{string};
> -            ",
> -            cfg = if builtin {
> -                "#[cfg(not(MODULE))]"
> -            } else {
> -                "#[cfg(MODULE)]"
> -            },
> +        let counter = format_ident!(
> +            "__{module}_{counter}",
>              module = self.module.to_uppercase(),
> -            counter = self.counter,
> -            length = string.len(),
> -            string = Literal::byte_string(string.as_bytes()),
> -        )
> -        .unwrap();
> +            counter = self.counter
> +        );
> +        let item = quote! {
> +            #cfg
> +            #[doc(hidden)]
> +            #[cfg_attr(not(target_os = "macos"), link_section = ".modinfo")]
> +            #[used(compiler)]
> +            pub static #counter: [u8; #length] = *#string;
> +        };
> +
> +        if param {
> +            self.param_ts.extend(item);
> +        } else {
> +            self.ts.extend(item);
> +        }
>
>          self.counter += 1;
>      }
> @@ -115,77 +119,86 @@ fn emit_params(&mut self, info: &ModuleInfo) {
>          };
>
>          for param in params {
> -            let param_name = param.name.to_string();
> -            let param_type = param.ptype.to_string();
> -            let param_default = param.default.to_token_stream().to_string();
> +            let param_name_str = param.name.to_string();
> +            let param_type_str = param.ptype.to_string();

This renaming is unfortunately; these variables were just introduced
in the previous patch.

>
> -            let ops = param_ops_path(&param_type);
> +            let ops = param_ops_path(&param_type_str);
>
>              // Note: The spelling of these fields is dictated by the user space
>              // tool `modinfo`.
> -            self.emit_param("parmtype", &param_name, &param_type);
> -            self.emit_param("parm", &param_name, &param.description.value());
> -
> -            write!(
> -                self.param_buffer,
> -                "
> -                pub(crate) static {param_name}:
> -                    ::kernel::module_param::ModuleParamAccess<{param_type}> =
> -                        ::kernel::module_param::ModuleParamAccess::new({param_default});
> -
> -                const _: () = {{
> -                    #[link_section = \"__param\"]
> -                    #[used]
> -                    static __{module_name}_{param_name}_struct:
> +            self.emit_param("parmtype", &param_name_str, &param_type_str);
> +            self.emit_param("parm", &param_name_str, &param.description.value());
> +
> +            let static_name = format_ident!("__{}_{}_struct", self.module, param.name);
> +            let param_name_cstr = Literal::c_string(
> +                &CString::new(param_name_str).expect("name contains NUL-terminator"),
> +            );
> +            let param_name_cstr_with_module = Literal::c_string(
> +                &CString::new(format!("{}.{}", self.module, param.name))
> +                    .expect("name contains NUL-terminator"),
> +            );
> +
> +            let param_name = &param.name;
> +            let param_type = &param.ptype;
> +            let param_default = &param.default;
> +
> +            self.param_ts.extend(quote!{
> +                #[allow(non_upper_case_globals)]
> +                pub(crate) static #param_name:
> +                    ::kernel::module_param::ModuleParamAccess<#param_type> =
> +                        ::kernel::module_param::ModuleParamAccess::new(#param_default);
> +
> +                const _: () = {
> +                    #[allow(non_upper_case_globals)]
> +                    #[link_section = "__param"]
> +                    #[used(compiler)]
> +                    static #static_name:
>                          ::kernel::module_param::KernelParam =
>                          ::kernel::module_param::KernelParam::new(
> -                            ::kernel::bindings::kernel_param {{
> -                                name: if ::core::cfg!(MODULE) {{
> -                                    ::kernel::c_str!(\"{param_name}\").to_bytes_with_nul()
> -                                }} else {{
> -                                    ::kernel::c_str!(\"{module_name}.{param_name}\")
> -                                        .to_bytes_with_nul()
> -                                }}.as_ptr(),
> +                            ::kernel::bindings::kernel_param {
> +                                name: kernel::str::as_char_ptr_in_const_context(
> +                                    if ::core::cfg!(MODULE) {
> +                                        #param_name_cstr
> +                                    } else {
> +                                        #param_name_cstr_with_module
> +                                    }
> +                                ),
>                                  // SAFETY: `__this_module` is constructed by the kernel at load
>                                  // time and will not be freed until the module is unloaded.
>                                  #[cfg(MODULE)]
> -                                mod_: unsafe {{
> +                                mod_: unsafe {
>                                      core::ptr::from_ref(&::kernel::bindings::__this_module)
>                                          .cast_mut()
> -                                }},
> +                                },
>                                  #[cfg(not(MODULE))]
>                                  mod_: ::core::ptr::null_mut(),
> -                                ops: core::ptr::from_ref(&{ops}),
> +                                ops: core::ptr::from_ref(&#ops),
>                                  perm: 0, // Will not appear in sysfs
>                                  level: -1,
>                                  flags: 0,
> -                                __bindgen_anon_1: ::kernel::bindings::kernel_param__bindgen_ty_1 {{
> -                                    arg: {param_name}.as_void_ptr()
> -                                }},
> -                            }}
> +                                __bindgen_anon_1: ::kernel::bindings::kernel_param__bindgen_ty_1 {
> +                                    arg: #param_name.as_void_ptr()
> +                                },
> +                            }
>                          );
> -                }};
> -                ",
> -                module_name = info.name.value(),
> -                ops = ops,
> -            )
> -            .unwrap();
> +                };
> +            });
>          }
>      }
>  }
>
> -fn param_ops_path(param_type: &str) -> &'static str {
> +fn param_ops_path(param_type: &str) -> Path {
>      match param_type {
> -        "i8" => "::kernel::module_param::PARAM_OPS_I8",
> -        "u8" => "::kernel::module_param::PARAM_OPS_U8",
> -        "i16" => "::kernel::module_param::PARAM_OPS_I16",
> -        "u16" => "::kernel::module_param::PARAM_OPS_U16",
> -        "i32" => "::kernel::module_param::PARAM_OPS_I32",
> -        "u32" => "::kernel::module_param::PARAM_OPS_U32",
> -        "i64" => "::kernel::module_param::PARAM_OPS_I64",
> -        "u64" => "::kernel::module_param::PARAM_OPS_U64",
> -        "isize" => "::kernel::module_param::PARAM_OPS_ISIZE",
> -        "usize" => "::kernel::module_param::PARAM_OPS_USIZE",
> +        "i8" => parse_quote!(::kernel::module_param::PARAM_OPS_I8),
> +        "u8" => parse_quote!(::kernel::module_param::PARAM_OPS_U8),
> +        "i16" => parse_quote!(::kernel::module_param::PARAM_OPS_I16),
> +        "u16" => parse_quote!(::kernel::module_param::PARAM_OPS_U16),
> +        "i32" => parse_quote!(::kernel::module_param::PARAM_OPS_I32),
> +        "u32" => parse_quote!(::kernel::module_param::PARAM_OPS_U32),
> +        "i64" => parse_quote!(::kernel::module_param::PARAM_OPS_I64),
> +        "u64" => parse_quote!(::kernel::module_param::PARAM_OPS_U64),
> +        "isize" => parse_quote!(::kernel::module_param::PARAM_OPS_ISIZE),
> +        "usize" => parse_quote!(::kernel::module_param::PARAM_OPS_USIZE),
>          t => panic!("Unsupported parameter type {}", t),
>      }
>  }
> @@ -424,29 +437,41 @@ fn parse(input: ParseStream<'_>) -> Result<Self> {
>  }
>
>  pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
> +    let ModuleInfo {
> +        type_,
> +        license,
> +        name,
> +        authors,
> +        description,
> +        alias,
> +        firmware,
> +        imports_ns,
> +        params: _,
> +    } = &info;
> +
>      // Rust does not allow hyphens in identifiers, use underscore instead.
> -    let ident = info.name.value().replace('-', "_");
> +    let ident = name.value().replace('-', "_");
>      let mut modinfo = ModInfoBuilder::new(ident.as_ref());
> -    if let Some(authors) = &info.authors {
> +    if let Some(authors) = authors {
>          for author in authors {
>              modinfo.emit("author", &author.value());
>          }
>      }
> -    if let Some(description) = &info.description {
> +    if let Some(description) = description {
>          modinfo.emit("description", &description.value());
>      }
> -    modinfo.emit("license", &info.license.value());
> -    if let Some(aliases) = &info.alias {
> +    modinfo.emit("license", &license.value());
> +    if let Some(aliases) = alias {
>          for alias in aliases {
>              modinfo.emit("alias", &alias.value());
>          }
>      }
> -    if let Some(firmware) = &info.firmware {
> +    if let Some(firmware) = firmware {
>          for fw in firmware {
>              modinfo.emit("firmware", &fw.value());
>          }
>      }
> -    if let Some(imports) = &info.imports_ns {
> +    if let Some(imports) = imports_ns {
>          for ns in imports {
>              modinfo.emit("import_ns", &ns.value());
>          }
> @@ -459,182 +484,189 @@ pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
>
>      modinfo.emit_params(&info);
>
> -    Ok(format!(
> -        "
> -            /// The module name.
> -            ///
> -            /// Used by the printing macros, e.g. [`info!`].
> -            const __LOG_PREFIX: &[u8] = b\"{name}\\0\";
> -
> -            // SAFETY: `__this_module` is constructed by the kernel at load time and will not be
> -            // freed until the module is unloaded.
> -            #[cfg(MODULE)]
> -            static THIS_MODULE: ::kernel::ThisModule = unsafe {{
> -                extern \"C\" {{
> -                    static __this_module: ::kernel::types::Opaque<::kernel::bindings::module>;
> -                }}
> -
> -                ::kernel::ThisModule::from_ptr(__this_module.get())
> -            }};
> -            #[cfg(not(MODULE))]
> -            static THIS_MODULE: ::kernel::ThisModule = unsafe {{
> -                ::kernel::ThisModule::from_ptr(::core::ptr::null_mut())
> -            }};
> -
> -            /// The `LocalModule` type is the type of the module created by `module!`,
> -            /// `module_pci_driver!`, `module_platform_driver!`, etc.
> -            type LocalModule = {type_};
> -
> -            impl ::kernel::ModuleMetadata for {type_} {{
> -                const NAME: &'static ::kernel::str::CStr = c\"{name}\";
> -            }}
> -
> -            // Double nested modules, since then nobody can access the public items inside.
> -            mod __module_init {{
> -                mod __module_init {{
> -                    use super::super::{type_};
> -                    use pin_init::PinInit;
> -
> -                    /// The \"Rust loadable module\" mark.
> -                    //
> -                    // This may be best done another way later on, e.g. as a new modinfo
> -                    // key or a new section. For the moment, keep it simple.
> -                    #[cfg(MODULE)]
> -                    #[doc(hidden)]
> -                    #[used(compiler)]
> -                    static __IS_RUST_MODULE: () = ();
> -
> -                    static mut __MOD: ::core::mem::MaybeUninit<{type_}> =
> -                        ::core::mem::MaybeUninit::uninit();
> -
> -                    // Loadable modules need to export the `{{init,cleanup}}_module` identifiers.
> -                    /// # Safety
> -                    ///
> -                    /// This function must not be called after module initialization, because it may be
> -                    /// freed after that completes.
> -                    #[cfg(MODULE)]
> -                    #[doc(hidden)]
> -                    #[no_mangle]
> -                    #[link_section = \".init.text\"]
> -                    pub unsafe extern \"C\" fn init_module() -> ::kernel::ffi::c_int {{
> -                        // SAFETY: This function is inaccessible to the outside due to the double
> -                        // module wrapping it. It is called exactly once by the C side via its
> -                        // unique name.
> -                        unsafe {{ __init() }}
> -                    }}
> -
> -                    #[cfg(MODULE)]
> -                    #[doc(hidden)]
> -                    #[used(compiler)]
> -                    #[link_section = \".init.data\"]
> -                    static __UNIQUE_ID___addressable_init_module: unsafe extern \"C\" fn() -> i32 = init_module;
> -
> -                    #[cfg(MODULE)]
> -                    #[doc(hidden)]
> -                    #[no_mangle]
> -                    #[link_section = \".exit.text\"]
> -                    pub extern \"C\" fn cleanup_module() {{
> -                        // SAFETY:
> -                        // - This function is inaccessible to the outside due to the double
> -                        //   module wrapping it. It is called exactly once by the C side via its
> -                        //   unique name,
> -                        // - furthermore it is only called after `init_module` has returned `0`
> -                        //   (which delegates to `__init`).
> -                        unsafe {{ __exit() }}
> -                    }}
> -
> -                    #[cfg(MODULE)]
> -                    #[doc(hidden)]
> -                    #[used(compiler)]
> -                    #[link_section = \".exit.data\"]
> -                    static __UNIQUE_ID___addressable_cleanup_module: extern \"C\" fn() = cleanup_module;
> -
> -                    // Built-in modules are initialized through an initcall pointer
> -                    // and the identifiers need to be unique.
> -                    #[cfg(not(MODULE))]
> -                    #[cfg(not(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS))]
> -                    #[doc(hidden)]
> -                    #[link_section = \"{initcall_section}\"]
> -                    #[used(compiler)]
> -                    pub static __{ident}_initcall: extern \"C\" fn() ->
> -                        ::kernel::ffi::c_int = __{ident}_init;
> -
> -                    #[cfg(not(MODULE))]
> -                    #[cfg(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS)]
> -                    ::core::arch::global_asm!(
> -                        r#\".section \"{initcall_section}\", \"a\"
> -                        __{ident}_initcall:
> -                            .long   __{ident}_init - .
> -                            .previous
> -                        \"#
> -                    );
> -
> -                    #[cfg(not(MODULE))]
> -                    #[doc(hidden)]
> -                    #[no_mangle]
> -                    pub extern \"C\" fn __{ident}_init() -> ::kernel::ffi::c_int {{
> -                        // SAFETY: This function is inaccessible to the outside due to the double
> -                        // module wrapping it. It is called exactly once by the C side via its
> -                        // placement above in the initcall section.
> -                        unsafe {{ __init() }}
> -                    }}
> -
> -                    #[cfg(not(MODULE))]
> -                    #[doc(hidden)]
> -                    #[no_mangle]
> -                    pub extern \"C\" fn __{ident}_exit() {{
> -                        // SAFETY:
> -                        // - This function is inaccessible to the outside due to the double
> -                        //   module wrapping it. It is called exactly once by the C side via its
> -                        //   unique name,
> -                        // - furthermore it is only called after `__{ident}_init` has
> -                        //   returned `0` (which delegates to `__init`).
> -                        unsafe {{ __exit() }}
> -                    }}
> -
> -                    /// # Safety
> -                    ///
> -                    /// This function must only be called once.
> -                    unsafe fn __init() -> ::kernel::ffi::c_int {{
> -                        let initer =
> -                            <{type_} as ::kernel::InPlaceModule>::init(&super::super::THIS_MODULE);
> -                        // SAFETY: No data race, since `__MOD` can only be accessed by this module
> -                        // and there only `__init` and `__exit` access it. These functions are only
> -                        // called once and `__exit` cannot be called before or during `__init`.
> -                        match unsafe {{ initer.__pinned_init(__MOD.as_mut_ptr()) }} {{
> -                            Ok(m) => 0,
> -                            Err(e) => e.to_errno(),
> -                        }}
> -                    }}
> -
> -                    /// # Safety
> -                    ///
> -                    /// This function must
> -                    /// - only be called once,
> -                    /// - be called after `__init` has been called and returned `0`.
> -                    unsafe fn __exit() {{
> -                        // SAFETY: No data race, since `__MOD` can only be accessed by this module
> -                        // and there only `__init` and `__exit` access it. These functions are only
> -                        // called once and `__init` was already called.
> -                        unsafe {{
> -                            // Invokes `drop()` on `__MOD`, which should be used for cleanup.
> -                            __MOD.assume_init_drop();
> -                        }}
> -                    }}
> -                    {modinfo}
> -                }}
> -            }}
> -            mod module_parameters {{
> -                {params}
> -            }}
> -        ",
> -        type_ = info.type_,
> -        name = info.name.value(),
> -        ident = ident,
> -        modinfo = modinfo.buffer,
> -        params = modinfo.param_buffer,
> -        initcall_section = ".initcall6.init"
> -    )
> -    .parse()
> -    .expect("Error parsing formatted string into token stream."))
> +    let modinfo_ts = modinfo.ts;
> +    let params_ts = modinfo.param_ts;
> +
> +    let ident_init = format_ident!("__{ident}_init");
> +    let ident_exit = format_ident!("__{ident}_exit");
> +    let ident_initcall = format_ident!("__{ident}_initcall");
> +    let initcall_section = ".initcall6.init";
> +
> +    let global_asm = format!(
> +        r#".section "{initcall_section}", "a"
> +        __{ident}_initcall:
> +            .long   __{ident}_init - .
> +            .previous
> +        "#
> +    );
> +
> +    let name_cstr =
> +        Literal::c_string(&CString::new(name.value()).expect("name contains NUL-terminator"));
> +
> +    Ok(quote! {
> +        /// The module name.
> +        ///
> +        /// Used by the printing macros, e.g. [`info!`].
> +        const __LOG_PREFIX: &[u8] = #name_cstr.to_bytes_with_nul();
> +
> +        // SAFETY: `__this_module` is constructed by the kernel at load time and will not be
> +        // freed until the module is unloaded.
> +        #[cfg(MODULE)]
> +        static THIS_MODULE: ::kernel::ThisModule = unsafe {
> +            extern "C" {
> +                static __this_module: ::kernel::types::Opaque<::kernel::bindings::module>;
> +            };
> +
> +            ::kernel::ThisModule::from_ptr(__this_module.get())
> +        };
> +
> +        #[cfg(not(MODULE))]
> +        static THIS_MODULE: ::kernel::ThisModule = unsafe {
> +            ::kernel::ThisModule::from_ptr(::core::ptr::null_mut())
> +        };
> +
> +        /// The `LocalModule` type is the type of the module created by `module!`,
> +        /// `module_pci_driver!`, `module_platform_driver!`, etc.
> +        type LocalModule = #type_;
> +
> +        impl ::kernel::ModuleMetadata for #type_ {
> +            const NAME: &'static ::kernel::str::CStr = #name_cstr;
> +        }
> +
> +        // Double nested modules, since then nobody can access the public items inside.
> +        mod __module_init {
> +            mod __module_init {
> +                use super::super::#type_;
> +                use pin_init::PinInit;
> +
> +                /// The "Rust loadable module" mark.
> +                //
> +                // This may be best done another way later on, e.g. as a new modinfo
> +                // key or a new section. For the moment, keep it simple.
> +                #[cfg(MODULE)]
> +                #[doc(hidden)]
> +                #[used(compiler)]
> +                static __IS_RUST_MODULE: () = ();
> +
> +                static mut __MOD: ::core::mem::MaybeUninit<#type_> =
> +                    ::core::mem::MaybeUninit::uninit();
> +
> +                // Loadable modules need to export the `{init,cleanup}_module` identifiers.
> +                /// # Safety
> +                ///
> +                /// This function must not be called after module initialization, because it may be
> +                /// freed after that completes.
> +                #[cfg(MODULE)]
> +                #[doc(hidden)]
> +                #[no_mangle]
> +                #[link_section = ".init.text"]
> +                pub unsafe extern "C" fn init_module() -> ::kernel::ffi::c_int {
> +                    // SAFETY: This function is inaccessible to the outside due to the double
> +                    // module wrapping it. It is called exactly once by the C side via its
> +                    // unique name.
> +                    unsafe { __init() }
> +                }
> +
> +                #[cfg(MODULE)]
> +                #[doc(hidden)]
> +                #[used(compiler)]
> +                #[link_section = ".init.data"]
> +                static __UNIQUE_ID___addressable_init_module: unsafe extern "C" fn() -> i32 =
> +                    init_module;
> +
> +                #[cfg(MODULE)]
> +                #[doc(hidden)]
> +                #[no_mangle]
> +                #[link_section = ".exit.text"]
> +                pub extern "C" fn cleanup_module() {
> +                    // SAFETY:
> +                    // - This function is inaccessible to the outside due to the double
> +                    //   module wrapping it. It is called exactly once by the C side via its
> +                    //   unique name,
> +                    // - furthermore it is only called after `init_module` has returned `0`
> +                    //   (which delegates to `__init`).
> +                    unsafe { __exit() }
> +                }
> +
> +                #[cfg(MODULE)]
> +                #[doc(hidden)]
> +                #[used(compiler)]
> +                #[link_section = ".exit.data"]
> +                static __UNIQUE_ID___addressable_cleanup_module: extern "C" fn() = cleanup_module;
> +
> +                // Built-in modules are initialized through an initcall pointer
> +                // and the identifiers need to be unique.
> +                #[cfg(not(MODULE))]
> +                #[cfg(not(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS))]
> +                #[doc(hidden)]
> +                #[link_section = #initcall_section]
> +                #[used(compiler)]
> +                pub static #ident_initcall: extern "C" fn() ->
> +                    ::kernel::ffi::c_int = #ident_initcall;
> +
> +                #[cfg(not(MODULE))]
> +                #[cfg(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS)]
> +                ::core::arch::global_asm!(#global_asm);
> +
> +                #[cfg(not(MODULE))]
> +                #[doc(hidden)]
> +                #[no_mangle]
> +                pub extern "C" fn #ident_init() -> ::kernel::ffi::c_int {
> +                    // SAFETY: This function is inaccessible to the outside due to the double
> +                    // module wrapping it. It is called exactly once by the C side via its
> +                    // placement above in the initcall section.
> +                    unsafe { __init() }
> +                }
> +
> +                #[cfg(not(MODULE))]
> +                #[doc(hidden)]
> +                #[no_mangle]
> +                pub extern "C" fn #ident_exit() {
> +                    // SAFETY:
> +                    // - This function is inaccessible to the outside due to the double
> +                    //   module wrapping it. It is called exactly once by the C side via its
> +                    //   unique name,
> +                    // - furthermore it is only called after `#ident_init` has
> +                    //   returned `0` (which delegates to `__init`).
> +                    unsafe { __exit() }
> +                }
> +
> +                /// # Safety
> +                ///
> +                /// This function must only be called once.
> +                unsafe fn __init() -> ::kernel::ffi::c_int {
> +                    let initer =
> +                        <#type_ as ::kernel::InPlaceModule>::init(&super::super::THIS_MODULE);
> +                    // SAFETY: No data race, since `__MOD` can only be accessed by this module
> +                    // and there only `__init` and `__exit` access it. These functions are only
> +                    // called once and `__exit` cannot be called before or during `__init`.
> +                    match unsafe { initer.__pinned_init(__MOD.as_mut_ptr()) } {
> +                        Ok(m) => 0,
> +                        Err(e) => e.to_errno(),
> +                    }
> +                }
> +
> +                /// # Safety
> +                ///
> +                /// This function must
> +                /// - only be called once,
> +                /// - be called after `__init` has been called and returned `0`.
> +                unsafe fn __exit() {
> +                    // SAFETY: No data race, since `__MOD` can only be accessed by this module
> +                    // and there only `__init` and `__exit` access it. These functions are only
> +                    // called once and `__init` was already called.
> +                    unsafe {
> +                        // Invokes `drop()` on `__MOD`, which should be used for cleanup.
> +                        __MOD.assume_init_drop();
> +                    }
> +                }
> +
> +                #modinfo_ts
> +            }
> +        }
> +
> +        mod module_parameters {
> +            #params_ts
> +        }
> +    })
>  }
> --
> 2.51.2
>
>

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 08/11] rust: macros: convert `#[kunit_tests]` macro to use `syn`
  2026-01-07 16:15 ` [PATCH v2 08/11] rust: macros: convert `#[kunit_tests]` macro " Gary Guo
@ 2026-01-07 17:22   ` Tamir Duberstein
  0 siblings, 0 replies; 23+ messages in thread
From: Tamir Duberstein @ 2026-01-07 17:22 UTC (permalink / raw)
  To: Gary Guo
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Brendan Higgins, David Gow, Rae Moar, José Expósito,
	rust-for-linux, Igor Korotin, linux-kselftest, kunit-dev,
	linux-kernel

On Wed, Jan 7, 2026 at 11:31 AM Gary Guo <gary@kernel.org> wrote:
>
> From: Gary Guo <gary@garyguo.net>
>
> Make use of `syn` to parse the module structurally and thus improve the
> robustness of parsing.
>
> String interpolation is avoided by generating tokens directly using
> `quote!`.
>
> Signed-off-by: Gary Guo <gary@garyguo.net>

Reviewed-by: Tamir Duberstein <tamird@gmail.com>


> ---
>  rust/macros/kunit.rs | 274 +++++++++++++++++++------------------------
>  rust/macros/lib.rs   |   6 +-
>  2 files changed, 123 insertions(+), 157 deletions(-)
>
> diff --git a/rust/macros/kunit.rs b/rust/macros/kunit.rs
> index 5cd6aa5eef07d..afbc708cbdc50 100644
> --- a/rust/macros/kunit.rs
> +++ b/rust/macros/kunit.rs
> @@ -4,81 +4,50 @@
>  //!
>  //! Copyright (c) 2023 José Expósito <jose.exposito89@gmail.com>
>
> -use std::collections::HashMap;
> -use std::fmt::Write;
> -
> -use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
> -
> -pub(crate) fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
> -    let attr = attr.to_string();
> -
> -    if attr.is_empty() {
> -        panic!("Missing test name in `#[kunit_tests(test_name)]` macro")
> -    }
> -
> -    if attr.len() > 255 {
> -        panic!("The test suite name `{attr}` exceeds the maximum length of 255 bytes")
> +use std::ffi::CString;
> +
> +use proc_macro2::TokenStream;
> +use quote::{
> +    format_ident,
> +    quote,
> +    ToTokens, //
> +};
> +use syn::{
> +    parse_quote,
> +    Error,
> +    Ident,
> +    Item,
> +    ItemMod,
> +    LitCStr,
> +    Result, //
> +};
> +
> +pub(crate) fn kunit_tests(test_suite: Ident, mut module: ItemMod) -> Result<TokenStream> {
> +    if test_suite.to_string().len() > 255 {
> +        return Err(Error::new_spanned(
> +            test_suite,
> +            "test suite names cannot exceed the maximum length of 255 bytes",
> +        ));
>      }
>
> -    let mut tokens: Vec<_> = ts.into_iter().collect();
> -
> -    // Scan for the `mod` keyword.
> -    tokens
> -        .iter()
> -        .find_map(|token| match token {
> -            TokenTree::Ident(ident) => match ident.to_string().as_str() {
> -                "mod" => Some(true),
> -                _ => None,
> -            },
> -            _ => None,
> -        })
> -        .expect("`#[kunit_tests(test_name)]` attribute should only be applied to modules");
> -
> -    // Retrieve the main body. The main body should be the last token tree.
> -    let body = match tokens.pop() {
> -        Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => group,
> -        _ => panic!("Cannot locate main body of module"),
> +    // We cannot handle modules that defer to another file (e.g. `mod foo;`).
> +    let Some((module_brace, module_items)) = module.content.take() else {
> +        Err(Error::new_spanned(
> +            module,
> +            "`#[kunit_tests(test_name)]` attribute should only be applied to inline modules",
> +        ))?
>      };
>
> -    // Get the functions set as tests. Search for `[test]` -> `fn`.
> -    let mut body_it = body.stream().into_iter();
> -    let mut tests = Vec::new();
> -    let mut attributes: HashMap<String, TokenStream> = HashMap::new();
> -    while let Some(token) = body_it.next() {
> -        match token {
> -            TokenTree::Punct(ref p) if p.as_char() == '#' => match body_it.next() {
> -                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => {
> -                    if let Some(TokenTree::Ident(name)) = g.stream().into_iter().next() {
> -                        // Collect attributes because we need to find which are tests. We also
> -                        // need to copy `cfg` attributes so tests can be conditionally enabled.
> -                        attributes
> -                            .entry(name.to_string())
> -                            .or_default()
> -                            .extend([token, TokenTree::Group(g)]);
> -                    }
> -                    continue;
> -                }
> -                _ => (),
> -            },
> -            TokenTree::Ident(i) if i == "fn" && attributes.contains_key("test") => {
> -                if let Some(TokenTree::Ident(test_name)) = body_it.next() {
> -                    tests.push((test_name, attributes.remove("cfg").unwrap_or_default()))
> -                }
> -            }
> -
> -            _ => (),
> -        }
> -        attributes.clear();
> -    }
> +    // Make the entire module gated behind `CONFIG_KUNIT`.
> +    module
> +        .attrs
> +        .insert(0, parse_quote!(#[cfg(CONFIG_KUNIT="y")]));
>
> -    // Add `#[cfg(CONFIG_KUNIT="y")]` before the module declaration.
> -    let config_kunit = "#[cfg(CONFIG_KUNIT=\"y\")]".to_owned().parse().unwrap();
> -    tokens.insert(
> -        0,
> -        TokenTree::Group(Group::new(Delimiter::None, config_kunit)),
> -    );
> +    let mut processed_items = Vec::new();
> +    let mut test_cases = Vec::new();
>
>      // Generate the test KUnit test suite and a test case for each `#[test]`.
> +    //
>      // The code generated for the following test module:
>      //
>      // ```
> @@ -110,98 +79,93 @@ pub(crate) fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
>      //
>      // ::kernel::kunit_unsafe_test_suite!(kunit_test_suit_name, TEST_CASES);
>      // ```
> -    let mut kunit_macros = "".to_owned();
> -    let mut test_cases = "".to_owned();
> -    let mut assert_macros = "".to_owned();
> -    let path = crate::helpers::file();
> -    let num_tests = tests.len();
> -    for (test, cfg_attr) in tests {
> -        let kunit_wrapper_fn_name = format!("kunit_rust_wrapper_{test}");
> -        // Append any `cfg` attributes the user might have written on their tests so we don't
> -        // attempt to call them when they are `cfg`'d out. An extra `use` is used here to reduce
> -        // the length of the assert message.
> -        let kunit_wrapper = format!(
> -            r#"unsafe extern "C" fn {kunit_wrapper_fn_name}(_test: *mut ::kernel::bindings::kunit)
> -            {{
> -                (*_test).status = ::kernel::bindings::kunit_status_KUNIT_SKIPPED;
> -                {cfg_attr} {{
> -                    (*_test).status = ::kernel::bindings::kunit_status_KUNIT_SUCCESS;
> -                    use ::kernel::kunit::is_test_result_ok;
> -                    assert!(is_test_result_ok({test}()));
> +    //
> +    // Non-function items (e.g. imports) are preserved.
> +    for item in module_items {
> +        let Item::Fn(mut f) = item else {
> +            processed_items.push(item);
> +            continue;
> +        };
> +
> +        // TODO: Replace below with `extract_if` when MSRV is bumped above 1.85.
> +        let before_len = f.attrs.len();
> +        f.attrs.retain(|attr| !attr.path().is_ident("test"));
> +        if f.attrs.len() == before_len {
> +            processed_items.push(Item::Fn(f));
> +            continue;
> +        }
> +
> +        let test = f.sig.ident.clone();
> +
> +        // Retrieve `#[cfg]` applied on the function which needs to be present on derived items too.
> +        let cfg_attrs: Vec<_> = f
> +            .attrs
> +            .iter()
> +            .filter(|attr| attr.path().is_ident("cfg"))
> +            .cloned()
> +            .collect();
> +
> +        // Before the test, override usual `assert!` and `assert_eq!` macros with ones that call
> +        // KUnit instead.
> +        let test_str = test.to_string();
> +        let path = crate::helpers::file();
> +        processed_items.push(parse_quote! {
> +            #[allow(unused)]
> +            macro_rules! assert {
> +                ($cond:expr $(,)?) => {{
> +                    kernel::kunit_assert!(#test_str, #path, 0, $cond);
> +                }}
> +            }
> +        });
> +        processed_items.push(parse_quote! {
> +            #[allow(unused)]
> +            macro_rules! assert_eq {
> +                ($left:expr, $right:expr $(,)?) => {{
> +                    kernel::kunit_assert_eq!(#test_str, #path, 0, $left, $right);
>                  }}
> -            }}"#,
> +            }
> +        });
> +
> +        // Add back the test item.
> +        processed_items.push(Item::Fn(f));
> +
> +        let kunit_wrapper_fn_name = format_ident!("kunit_rust_wrapper_{test}");
> +        let test_cstr = LitCStr::new(
> +            &CString::new(test_str.as_str()).expect("identifier cannot contain NUL"),
> +            test.span(),
>          );
> -        writeln!(kunit_macros, "{kunit_wrapper}").unwrap();
> -        writeln!(
> -            test_cases,
> -            "    ::kernel::kunit::kunit_case(::kernel::c_str!(\"{test}\"), {kunit_wrapper_fn_name}),"
> -        )
> -        .unwrap();
> -        writeln!(
> -            assert_macros,
> -            r#"
> -/// Overrides the usual [`assert!`] macro with one that calls KUnit instead.
> -#[allow(unused)]
> -macro_rules! assert {{
> -    ($cond:expr $(,)?) => {{{{
> -        kernel::kunit_assert!("{test}", "{path}", 0, $cond);
> -    }}}}
> -}}
> -
> -/// Overrides the usual [`assert_eq!`] macro with one that calls KUnit instead.
> -#[allow(unused)]
> -macro_rules! assert_eq {{
> -    ($left:expr, $right:expr $(,)?) => {{{{
> -        kernel::kunit_assert_eq!("{test}", "{path}", 0, $left, $right);
> -    }}}}
> -}}
> -        "#
> -        )
> -        .unwrap();
> -    }
> +        processed_items.push(parse_quote! {
> +            unsafe extern "C" fn #kunit_wrapper_fn_name(_test: *mut ::kernel::bindings::kunit) {
> +                (*_test).status = ::kernel::bindings::kunit_status_KUNIT_SKIPPED;
>
> -    writeln!(kunit_macros).unwrap();
> -    writeln!(
> -        kunit_macros,
> -        "static mut TEST_CASES: [::kernel::bindings::kunit_case; {}] = [\n{test_cases}    ::kernel::kunit::kunit_case_null(),\n];",
> -        num_tests + 1
> -    )
> -    .unwrap();
> -
> -    writeln!(
> -        kunit_macros,
> -        "::kernel::kunit_unsafe_test_suite!({attr}, TEST_CASES);"
> -    )
> -    .unwrap();
> -
> -    // Remove the `#[test]` macros.
> -    // We do this at a token level, in order to preserve span information.
> -    let mut new_body = vec![];
> -    let mut body_it = body.stream().into_iter();
> -
> -    while let Some(token) = body_it.next() {
> -        match token {
> -            TokenTree::Punct(ref c) if c.as_char() == '#' => match body_it.next() {
> -                Some(TokenTree::Group(group)) if group.to_string() == "[test]" => (),
> -                Some(next) => {
> -                    new_body.extend([token, next]);
> -                }
> -                _ => {
> -                    new_body.push(token);
> +                // Append any `cfg` attributes the user might have written on their tests so we
> +                // don't attempt to call them when they are `cfg`'d out. An extra `use` is used
> +                // here to reduce the length of the assert message.
> +                #(#cfg_attrs)*
> +                {
> +                    (*_test).status = ::kernel::bindings::kunit_status_KUNIT_SUCCESS;
> +                    use ::kernel::kunit::is_test_result_ok;
> +                    assert!(is_test_result_ok(#test()));
>                  }
> -            },
> -            _ => {
> -                new_body.push(token);
>              }
> -        }
> -    }
> +        });
>
> -    let mut final_body = TokenStream::new();
> -    final_body.extend::<TokenStream>(assert_macros.parse().unwrap());
> -    final_body.extend(new_body);
> -    final_body.extend::<TokenStream>(kunit_macros.parse().unwrap());
> -
> -    tokens.push(TokenTree::Group(Group::new(Delimiter::Brace, final_body)));
> +        test_cases.push(quote!(
> +            ::kernel::kunit::kunit_case(#test_cstr, #kunit_wrapper_fn_name)
> +        ));
> +    }
>
> -    tokens.into_iter().collect()
> +    let num_tests_plus_1 = test_cases.len() + 1;
> +    processed_items.push(parse_quote! {
> +        static mut TEST_CASES: [::kernel::bindings::kunit_case; #num_tests_plus_1] = [
> +            #(#test_cases,)*
> +            ::kernel::kunit::kunit_case_null(),
> +        ];
> +    });
> +    processed_items.push(parse_quote! {
> +        ::kernel::kunit_unsafe_test_suite!(#test_suite, TEST_CASES);
> +    });
> +
> +    module.content = Some((module_brace, processed_items));
> +    Ok(module.to_token_stream())
>  }
> diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
> index 12467bfc703a8..75ac60abe6ffa 100644
> --- a/rust/macros/lib.rs
> +++ b/rust/macros/lib.rs
> @@ -481,6 +481,8 @@ pub fn paste(input: TokenStream) -> TokenStream {
>  /// }
>  /// ```
>  #[proc_macro_attribute]
> -pub fn kunit_tests(attr: TokenStream, ts: TokenStream) -> TokenStream {
> -    kunit::kunit_tests(attr.into(), ts.into()).into()
> +pub fn kunit_tests(attr: TokenStream, input: TokenStream) -> TokenStream {
> +    kunit::kunit_tests(parse_macro_input!(attr), parse_macro_input!(input))
> +        .unwrap_or_else(|e| e.into_compile_error())
> +        .into()
>  }
> --
> 2.51.2
>

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 05/11] rust: macros: use `quote!` for `module!` macro
  2026-01-07 17:19   ` Tamir Duberstein
@ 2026-01-07 17:54     ` Gary Guo
  0 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-07 17:54 UTC (permalink / raw)
  To: Tamir Duberstein
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, rust-for-linux, linux-modules, linux-kernel

On Wed, 7 Jan 2026 12:19:26 -0500
Tamir Duberstein <tamird@gmail.com> wrote:

> On Wed, Jan 7, 2026 at 11:59 AM Gary Guo <gary@kernel.org> wrote:
> >
> > From: Gary Guo <gary@garyguo.net>
> >
> > This has no behavioural change, but is good for maintainability. With
> > `quote!`, we're no longer using string templates, so we don't need to
> > quote " and {} inside the template anymore. Further more, editors can
> > now highlight the code template.
> >
> > This also improves the robustness as it eliminates the need for string
> > quoting and escaping.
> >
> > Co-developed-by: Benno Lossin <lossin@kernel.org>
> > Signed-off-by: Benno Lossin <lossin@kernel.org>
> > Signed-off-by: Gary Guo <gary@garyguo.net>  
> 
> Are there significant changes here? You didn't pick up my tag.

There're changes related to added `params` which you probably already
noticed.

Best,
Gary

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn`
  2026-01-07 16:48   ` Tamir Duberstein
@ 2026-01-08 12:41     ` Gary Guo
  2026-01-08 15:10       ` Tamir Duberstein
  0 siblings, 1 reply; 23+ messages in thread
From: Gary Guo @ 2026-01-08 12:41 UTC (permalink / raw)
  To: Tamir Duberstein
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Igor Korotin, José Expósito, rust-for-linux,
	linux-kernel

On Wed, 7 Jan 2026 11:48:28 -0500
Tamir Duberstein <tamird@gmail.com> wrote:

> On Wed, Jan 7, 2026 at 11:30 AM Gary Guo <gary@kernel.org> wrote:
> >
> > From: Gary Guo <gary@garyguo.net>
> >
> > `#[vtable]` is converted to use syn. This is more robust than the
> > previous heuristic-based searching of defined methods and functions.
> >
> > When doing so, the trait and impl are split into two code paths as the
> > types are distinct when parsed by `syn`.
> >
> > Signed-off-by: Gary Guo <gary@garyguo.net>  
> 
> Reviewed-by: Tamir Duberstein <tamird@gmail.com>
> 
> > ---
> >  rust/macros/lib.rs    |   9 ++-
> >  rust/macros/vtable.rs | 163 ++++++++++++++++++++++--------------------
> >  2 files changed, 93 insertions(+), 79 deletions(-)
> >
> > diff --git a/rust/macros/vtable.rs b/rust/macros/vtable.rs
> > index a67d1cc81a2d3..a39bedb703973 100644
> > --- a/rust/macros/vtable.rs
> > +++ b/rust/macros/vtable.rs
> > @@ -1,97 +1,106 @@
> >  // SPDX-License-Identifier: GPL-2.0
> >
> >  use std::collections::HashSet;
> > -use std::fmt::Write;
> >
> > -use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
> > +use proc_macro2::{
> > +    Ident,
> > +    TokenStream, //
> > +};
> > +use quote::ToTokens;
> > +use syn::{
> > +    parse_quote,
> > +    Error,
> > +    ImplItem,
> > +    Item,
> > +    ItemImpl,
> > +    ItemTrait,
> > +    Result,
> > +    TraitItem, //
> > +};
> >
> > -pub(crate) fn vtable(_attr: TokenStream, ts: TokenStream) -> TokenStream {
> > -    let mut tokens: Vec<_> = ts.into_iter().collect();
> > +fn handle_trait(mut item: ItemTrait) -> Result<ItemTrait> {
> > +    let mut functions = Vec::new();  
> 
> Would be easier to propagate `#[cfg]`(as in your FIXME below) if you
> collect the generated constants into this vector instead -- then you
> can do all the code generation in the `for item in &item.items` loop
> where you have all the attributes.

This is a very good suggestion. I tried it out and the code is much
cleaner. After making the change the `#[cfg]` change is just a few lines
too -- I'll include the cfg change in a new commit in the next version.

> 
> > +    for item in &item.items {
> > +        if let TraitItem::Fn(fn_item) = item {
> > +            functions.push(fn_item.sig.ident.clone());
> > +        }
> > +    }
> > +
> > +    item.items.push(parse_quote! {
> > +         /// A marker to prevent implementors from forgetting to use [`#[vtable]`](vtable)
> > +         /// attribute when implementing this trait.
> > +         const USE_VTABLE_ATTR: ();
> > +    });
> >
> > -    // Scan for the `trait` or `impl` keyword.
> > -    let is_trait = tokens
> > -        .iter()
> > -        .find_map(|token| match token {
> > -            TokenTree::Ident(ident) => match ident.to_string().as_str() {
> > -                "trait" => Some(true),
> > -                "impl" => Some(false),
> > -                _ => None,
> > -            },
> > -            _ => None,
> > -        })
> > -        .expect("#[vtable] attribute should only be applied to trait or impl block");
> > +    let mut consts = HashSet::new();
> > +    for name in functions {
> > +        let gen_const_name = Ident::new(
> > +            &format!("HAS_{}", name.to_string().to_uppercase()),
> > +            name.span(),
> > +        );
> > +        // Skip if it's declared already -- this can happen if `#[cfg]` is used to selectively
> > +        // define functions.
> > +        // FIXME: `#[cfg]` should be copied and propagated to the generated consts.
> > +        if consts.contains(&gen_const_name) {
> > +            continue;
> > +        }
> > +        // We don't know on the implementation-site whether a method is required or provided
> > +        // so we have to generate a const for all methods.
> > +        let comment = format!("Indicates if the `{name}` method is overridden by the implementor.");  
> 
> We're already quasi-quoting below, does putting the comment there work?

The comment has `{name}` interpolation and you cannot just use `quote!`
for it.

You could do

	#[doc = concat!("...", stringify!(...), "...")]

But I think it's cleaner to just use `format!`.

> 
> > +        item.items.push(parse_quote! {
> > +            #[doc = #comment]
> > +            const #gen_const_name: bool = false;
> > +        });
> > +        consts.insert(gen_const_name);
> > +    }

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn`
  2026-01-08 12:41     ` Gary Guo
@ 2026-01-08 15:10       ` Tamir Duberstein
  2026-01-08 15:24         ` Gary Guo
  0 siblings, 1 reply; 23+ messages in thread
From: Tamir Duberstein @ 2026-01-08 15:10 UTC (permalink / raw)
  To: Gary Guo
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Igor Korotin, José Expósito, rust-for-linux,
	linux-kernel

On Thu, Jan 8, 2026 at 7:41 AM Gary Guo <gary@garyguo.net> wrote:
>
> On Wed, 7 Jan 2026 11:48:28 -0500
> Tamir Duberstein <tamird@gmail.com> wrote:
>
> > On Wed, Jan 7, 2026 at 11:30 AM Gary Guo <gary@kernel.org> wrote:
> > >
> > > From: Gary Guo <gary@garyguo.net>
> > >
> > > `#[vtable]` is converted to use syn. This is more robust than the
> > > previous heuristic-based searching of defined methods and functions.
> > >
> > > When doing so, the trait and impl are split into two code paths as the
> > > types are distinct when parsed by `syn`.
> > >
> > > Signed-off-by: Gary Guo <gary@garyguo.net>
> >
> > Reviewed-by: Tamir Duberstein <tamird@gmail.com>
> >
> > > ---
> > >  rust/macros/lib.rs    |   9 ++-
> > >  rust/macros/vtable.rs | 163 ++++++++++++++++++++++--------------------
> > >  2 files changed, 93 insertions(+), 79 deletions(-)
> > >
> > > diff --git a/rust/macros/vtable.rs b/rust/macros/vtable.rs
> > > index a67d1cc81a2d3..a39bedb703973 100644
> > > --- a/rust/macros/vtable.rs
> > > +++ b/rust/macros/vtable.rs
> > > @@ -1,97 +1,106 @@
> > >  // SPDX-License-Identifier: GPL-2.0
> > >
> > >  use std::collections::HashSet;
> > > -use std::fmt::Write;
> > >
> > > -use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
> > > +use proc_macro2::{
> > > +    Ident,
> > > +    TokenStream, //
> > > +};
> > > +use quote::ToTokens;
> > > +use syn::{
> > > +    parse_quote,
> > > +    Error,
> > > +    ImplItem,
> > > +    Item,
> > > +    ItemImpl,
> > > +    ItemTrait,
> > > +    Result,
> > > +    TraitItem, //
> > > +};
> > >
> > > -pub(crate) fn vtable(_attr: TokenStream, ts: TokenStream) -> TokenStream {
> > > -    let mut tokens: Vec<_> = ts.into_iter().collect();
> > > +fn handle_trait(mut item: ItemTrait) -> Result<ItemTrait> {
> > > +    let mut functions = Vec::new();
> >
> > Would be easier to propagate `#[cfg]`(as in your FIXME below) if you
> > collect the generated constants into this vector instead -- then you
> > can do all the code generation in the `for item in &item.items` loop
> > where you have all the attributes.
>
> This is a very good suggestion. I tried it out and the code is much
> cleaner. After making the change the `#[cfg]` change is just a few lines
> too -- I'll include the cfg change in a new commit in the next version.

Great.

> >
> > > +    for item in &item.items {
> > > +        if let TraitItem::Fn(fn_item) = item {
> > > +            functions.push(fn_item.sig.ident.clone());
> > > +        }
> > > +    }
> > > +
> > > +    item.items.push(parse_quote! {
> > > +         /// A marker to prevent implementors from forgetting to use [`#[vtable]`](vtable)
> > > +         /// attribute when implementing this trait.
> > > +         const USE_VTABLE_ATTR: ();
> > > +    });
> > >
> > > -    // Scan for the `trait` or `impl` keyword.
> > > -    let is_trait = tokens
> > > -        .iter()
> > > -        .find_map(|token| match token {
> > > -            TokenTree::Ident(ident) => match ident.to_string().as_str() {
> > > -                "trait" => Some(true),
> > > -                "impl" => Some(false),
> > > -                _ => None,
> > > -            },
> > > -            _ => None,
> > > -        })
> > > -        .expect("#[vtable] attribute should only be applied to trait or impl block");
> > > +    let mut consts = HashSet::new();
> > > +    for name in functions {
> > > +        let gen_const_name = Ident::new(
> > > +            &format!("HAS_{}", name.to_string().to_uppercase()),
> > > +            name.span(),
> > > +        );
> > > +        // Skip if it's declared already -- this can happen if `#[cfg]` is used to selectively
> > > +        // define functions.
> > > +        // FIXME: `#[cfg]` should be copied and propagated to the generated consts.
> > > +        if consts.contains(&gen_const_name) {
> > > +            continue;
> > > +        }
> > > +        // We don't know on the implementation-site whether a method is required or provided
> > > +        // so we have to generate a const for all methods.
> > > +        let comment = format!("Indicates if the `{name}` method is overridden by the implementor.");
> >
> > We're already quasi-quoting below, does putting the comment there work?
>
> The comment has `{name}` interpolation and you cannot just use `quote!`
> for it.
>
> You could do
>
>         #[doc = concat!("...", stringify!(...), "...")]
>
> But I think it's cleaner to just use `format!`.

Ack, though I think it would be:

#[doc = concat!("Indicates if the `", #name, "` method is overridden
by the implementor.")]

i.e. no need for `stringify`.

>
> >
> > > +        item.items.push(parse_quote! {
> > > +            #[doc = #comment]
> > > +            const #gen_const_name: bool = false;
> > > +        });
> > > +        consts.insert(gen_const_name);
> > > +    }

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn`
  2026-01-08 15:10       ` Tamir Duberstein
@ 2026-01-08 15:24         ` Gary Guo
  0 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-08 15:24 UTC (permalink / raw)
  To: Tamir Duberstein
  Cc: Miguel Ojeda, Boqun Feng, Björn Roy Baron, Benno Lossin,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Igor Korotin, José Expósito, rust-for-linux,
	linux-kernel

On Thu, 8 Jan 2026 10:10:23 -0500
Tamir Duberstein <tamird@gmail.com> wrote:

> > > > +    let mut consts = HashSet::new();
> > > > +    for name in functions {
> > > > +        let gen_const_name = Ident::new(
> > > > +            &format!("HAS_{}", name.to_string().to_uppercase()),
> > > > +            name.span(),
> > > > +        );
> > > > +        // Skip if it's declared already -- this can happen if `#[cfg]` is used to selectively
> > > > +        // define functions.
> > > > +        // FIXME: `#[cfg]` should be copied and propagated to the generated consts.
> > > > +        if consts.contains(&gen_const_name) {
> > > > +            continue;
> > > > +        }
> > > > +        // We don't know on the implementation-site whether a method is required or provided
> > > > +        // so we have to generate a const for all methods.
> > > > +        let comment = format!("Indicates if the `{name}` method is overridden by the implementor.");  
> > >
> > > We're already quasi-quoting below, does putting the comment there work?  
> >
> > The comment has `{name}` interpolation and you cannot just use `quote!`
> > for it.
> >
> > You could do
> >
> >         #[doc = concat!("...", stringify!(...), "...")]
> >
> > But I think it's cleaner to just use `format!`.  
> 
> Ack, though I think it would be:
> 
> #[doc = concat!("Indicates if the `", #name, "` method is overridden
> by the implementor.")]
> 
> i.e. no need for `stringify`.

Note `name` here is an `Ident`, which gets turned into an identifer, not a
string, so if `concat!` is used stringify is still necessary.

Anyway, I'm going to keep `format!` in the new version.

Best,
Gary

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn`
  2026-01-07 16:15 ` [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn` Gary Guo
  2026-01-07 16:48   ` Tamir Duberstein
@ 2026-01-11 17:03   ` Benno Lossin
  2026-01-11 21:25     ` Gary Guo
  1 sibling, 1 reply; 23+ messages in thread
From: Benno Lossin @ 2026-01-11 17:03 UTC (permalink / raw)
  To: Gary Guo, Miguel Ojeda, Boqun Feng, Björn Roy Baron,
	Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
	Tamir Duberstein, Igor Korotin, José Expósito
  Cc: rust-for-linux, linux-kernel

On Wed Jan 7, 2026 at 5:15 PM CET, Gary Guo wrote:
> From: Gary Guo <gary@garyguo.net>
>
> `#[vtable]` is converted to use syn. This is more robust than the
> previous heuristic-based searching of defined methods and functions.
>
> When doing so, the trait and impl are split into two code paths as the
> types are distinct when parsed by `syn`.
>
> Signed-off-by: Gary Guo <gary@garyguo.net>

Reviewed-by: Benno Lossin <lossin@kernel.org>

> ---
>  rust/macros/lib.rs    |   9 ++-
>  rust/macros/vtable.rs | 163 ++++++++++++++++++++++--------------------
>  2 files changed, 93 insertions(+), 79 deletions(-)

> +        // Skip if it's declared already -- this can happen if `#[cfg]` is used to selectively
> +        // define functions.
> +        // FIXME: `#[cfg]` should be copied and propagated to the generated consts.

Do you mind creating an issue (probably difficulty medium) after this is
merged?

Cheers,
Benno

> +        if consts.contains(&gen_const_name) {
> +            continue;
> +        }

^ permalink raw reply	[flat|nested] 23+ messages in thread

* Re: [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn`
  2026-01-11 17:03   ` Benno Lossin
@ 2026-01-11 21:25     ` Gary Guo
  0 siblings, 0 replies; 23+ messages in thread
From: Gary Guo @ 2026-01-11 21:25 UTC (permalink / raw)
  To: Benno Lossin, Gary Guo, Miguel Ojeda, Boqun Feng,
	Björn Roy Baron, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, Tamir Duberstein, Igor Korotin,
	José Expósito
  Cc: rust-for-linux, linux-kernel

On Sun Jan 11, 2026 at 5:03 PM GMT, Benno Lossin wrote:
> On Wed Jan 7, 2026 at 5:15 PM CET, Gary Guo wrote:
>> From: Gary Guo <gary@garyguo.net>
>>
>> `#[vtable]` is converted to use syn. This is more robust than the
>> previous heuristic-based searching of defined methods and functions.
>>
>> When doing so, the trait and impl are split into two code paths as the
>> types are distinct when parsed by `syn`.
>>
>> Signed-off-by: Gary Guo <gary@garyguo.net>
>
> Reviewed-by: Benno Lossin <lossin@kernel.org>
>
>> ---
>>  rust/macros/lib.rs    |   9 ++-
>>  rust/macros/vtable.rs | 163 ++++++++++++++++++++++--------------------
>>  2 files changed, 93 insertions(+), 79 deletions(-)
>
>> +        // Skip if it's declared already -- this can happen if `#[cfg]` is used to selectively
>> +        // define functions.
>> +        // FIXME: `#[cfg]` should be copied and propagated to the generated consts.
>
> Do you mind creating an issue (probably difficulty medium) after this is
> merged?

I've already had a patch for this in my working branch. Following Tamir's
suggestion on how to change this patch it was trivially enough just add to the
series.

It'll be included in v3.

Best,
Gary

^ permalink raw reply	[flat|nested] 23+ messages in thread

end of thread, other threads:[~2026-01-11 21:25 UTC | newest]

Thread overview: 23+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-07 16:15 [PATCH v2 00/11] refactor Rust proc macros with `syn` Gary Guo
2026-01-07 16:15 ` [PATCH v2 01/11] rust: pin-init: internal: remove proc-macro[2] and quote workarounds Gary Guo
2026-01-07 16:40   ` Tamir Duberstein
2026-01-07 16:15 ` [PATCH v2 02/11] rust: macros: use `quote!` from vendored crate Gary Guo
2026-01-07 16:15 ` [PATCH v2 03/11] rust: macros: convert `#[vtable]` macro to use `syn` Gary Guo
2026-01-07 16:48   ` Tamir Duberstein
2026-01-08 12:41     ` Gary Guo
2026-01-08 15:10       ` Tamir Duberstein
2026-01-08 15:24         ` Gary Guo
2026-01-11 17:03   ` Benno Lossin
2026-01-11 21:25     ` Gary Guo
2026-01-07 16:15 ` [PATCH v2 04/11] rust: macros: use `syn` to parse `module!` macro Gary Guo
2026-01-07 17:11   ` Tamir Duberstein
2026-01-07 16:15 ` [PATCH v2 05/11] rust: macros: use `quote!` for " Gary Guo
2026-01-07 17:19   ` Tamir Duberstein
2026-01-07 17:54     ` Gary Guo
2026-01-07 16:15 ` [PATCH v2 06/11] rust: macros: convert `#[export]` to use `syn` Gary Guo
2026-01-07 16:15 ` [PATCH v2 07/11] rust: macros: convert `concat_idents!` " Gary Guo
2026-01-07 16:15 ` [PATCH v2 08/11] rust: macros: convert `#[kunit_tests]` macro " Gary Guo
2026-01-07 17:22   ` Tamir Duberstein
2026-01-07 16:15 ` [PATCH v2 09/11] rust: macros: allow arbitrary types to be used in `module!` macro Gary Guo
2026-01-07 16:15 ` [PATCH v2 10/11] rust: macros: rearrange `#[doc(hidden)]` " Gary Guo
2026-01-07 16:15 ` [PATCH v2 11/11] rust: kunit: use `pin_init::zeroed` instead of custom null value Gary Guo

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox