rust-for-linux.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Gary Guo <gary@kernel.org>
To: "Miguel Ojeda" <ojeda@kernel.org>,
	"Boqun Feng" <boqun.feng@gmail.com>,
	"Gary Guo" <gary@garyguo.net>,
	"Björn Roy Baron" <bjorn3_gh@protonmail.com>,
	"Benno Lossin" <lossin@kernel.org>,
	"Andreas Hindborg" <a.hindborg@kernel.org>,
	"Alice Ryhl" <aliceryhl@google.com>,
	"Trevor Gross" <tmgross@umich.edu>,
	"Danilo Krummrich" <dakr@kernel.org>,
	"Fiona Behrens" <me@kloenk.dev>,
	"Tamir Duberstein" <tamird@gmail.com>,
	"Patrick Miller" <paddymills@proton.me>,
	"José Expósito" <jose.exposito89@gmail.com>,
	"Greg Kroah-Hartman" <gregkh@linuxfoundation.org>,
	"Guilherme Giacomo Simoes" <trintaeoitogc@gmail.com>
Cc: rust-for-linux@vger.kernel.org,
	Igor Korotin <igor.korotin.linux@gmail.com>,
	linux-kernel@vger.kernel.org
Subject: [PATCH 04/11] rust: macros: use `syn` to parse `module!` macro
Date: Thu, 11 Dec 2025 18:56:44 +0000	[thread overview]
Message-ID: <20251211185805.2835633-5-gary@kernel.org> (raw)
In-Reply-To: <20251211185805.2835633-1-gary@kernel.org>

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",
       |     ^^^^^^^

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/helpers.rs |  84 ++++++----------
 rust/macros/lib.rs     |   9 +-
 rust/macros/module.rs  | 220 ++++++++++++++++++++++++++++++-----------
 3 files changed, 202 insertions(+), 111 deletions(-)

diff --git a/rust/macros/helpers.rs b/rust/macros/helpers.rs
index 853527b5d9567..f6bbdb02d9d7d 100644
--- a/rust/macros/helpers.rs
+++ b/rust/macros/helpers.rs
@@ -1,42 +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_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") {
@@ -46,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()
     }
 }
 
diff --git a/rust/macros/lib.rs b/rust/macros/lib.rs
index 33935b38d11c0..4e440deaed853 100644
--- a/rust/macros/lib.rs
+++ b/rust/macros/lib.rs
@@ -10,6 +10,9 @@
 // which was added in Rust 1.88.0. This is why `cfg_attr` is used here, i.e.
 // 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))]
+//
+// Stable since Rust 1.81.0.
+#![feature(lint_reasons)]
 
 mod concat_idents;
 mod export;
@@ -100,8 +103,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 6974fb04f58fa..e4d51878a5360 100644
--- a/rust/macros/module.rs
+++ b/rust/macros/module.rs
@@ -2,28 +2,27 @@
 
 use std::fmt::Write;
 
-use proc_macro2::{token_stream, Delimiter, Literal, TokenStream, TokenTree};
+use proc_macro2::{
+    Literal,
+    TokenStream, //
+};
+use syn::{
+    bracketed,
+    parse::{
+        Parse,
+        ParseStream, //
+    },
+    punctuated::Punctuated,
+    token::Bracket,
+    Error,
+    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,
@@ -91,8 +90,107 @@ fn emit(&mut self, field: &str, content: &str) {
     }
 }
 
+mod kw {
+    syn::custom_keyword!(name);
+    syn::custom_keyword!(authors);
+    syn::custom_keyword!(description);
+    syn::custom_keyword!(license);
+    syn::custom_keyword!(alias);
+    syn::custom_keyword!(firmware);
+}
+
+#[allow(dead_code, reason = "some fields are only parsed into")]
+enum ModInfoField {
+    Type(Token![type], Token![:], Ident),
+    Name(kw::name, Token![:], AsciiLitStr),
+    Authors(
+        kw::authors,
+        Token![:],
+        Bracket,
+        Punctuated<LitStr, Token![,]>,
+    ),
+    Description(kw::description, Token![:], LitStr),
+    License(kw::license, Token![:], AsciiLitStr),
+    Alias(
+        kw::authors,
+        Token![:],
+        Bracket,
+        Punctuated<LitStr, Token![,]>,
+    ),
+    Firmware(
+        kw::firmware,
+        Token![:],
+        Bracket,
+        Punctuated<LitStr, Token![,]>,
+    ),
+}
+
+impl ModInfoField {
+    /// Obtain the key identifying the field.
+    fn key(&self) -> Ident {
+        match self {
+            ModInfoField::Type(key, ..) => Ident::new("type", key.span),
+            ModInfoField::Name(key, ..) => Ident::new("name", key.span),
+            ModInfoField::Authors(key, ..) => Ident::new("authors", key.span),
+            ModInfoField::Description(key, ..) => Ident::new("description", key.span),
+            ModInfoField::License(key, ..) => Ident::new("license", key.span),
+            ModInfoField::Alias(key, ..) => Ident::new("alias", key.span),
+            ModInfoField::Firmware(key, ..) => Ident::new("firmware", key.span),
+        }
+    }
+}
+
+impl Parse for ModInfoField {
+    fn parse(input: ParseStream<'_>) -> Result<Self> {
+        let key = input.lookahead1();
+        if key.peek(Token![type]) {
+            Ok(Self::Type(input.parse()?, input.parse()?, input.parse()?))
+        } else if key.peek(kw::name) {
+            Ok(Self::Name(input.parse()?, input.parse()?, input.parse()?))
+        } else if key.peek(kw::authors) {
+            let list;
+            Ok(Self::Authors(
+                input.parse()?,
+                input.parse()?,
+                bracketed!(list in input),
+                Punctuated::parse_terminated(&list)?,
+            ))
+        } else if key.peek(kw::description) {
+            Ok(Self::Description(
+                input.parse()?,
+                input.parse()?,
+                input.parse()?,
+            ))
+        } else if key.peek(kw::license) {
+            Ok(Self::License(
+                input.parse()?,
+                input.parse()?,
+                input.parse()?,
+            ))
+        } else if key.peek(kw::alias) {
+            let list;
+            Ok(Self::Alias(
+                input.parse()?,
+                input.parse()?,
+                bracketed!(list in input),
+                Punctuated::parse_terminated(&list)?,
+            ))
+        } else if key.peek(kw::firmware) {
+            let list;
+            Ok(Self::Firmware(
+                input.parse()?,
+                input.parse()?,
+                bracketed!(list in input),
+                Punctuated::parse_terminated(&list)?,
+            ))
+        } else {
+            Err(key.error())
+        }
+    }
+}
+
 #[derive(Debug, Default)]
-struct ModuleInfo {
+pub(crate) struct ModuleInfo {
     type_: String,
     license: String,
     name: String,
@@ -102,9 +200,13 @@ struct ModuleInfo {
     firmware: Option<Vec<String>>,
 }
 
-impl ModuleInfo {
-    fn parse(it: &mut token_stream::IntoIter) -> Self {
-        let mut info = ModuleInfo::default();
+impl Parse for ModuleInfo {
+    fn parse(input: ParseStream<'_>) -> Result<Self> {
+        let mut info = Self::default();
+
+        let span = input.span();
+        let fields = Punctuated::<ModInfoField, Token![,]>::parse_terminated(input)?;
+        let mut errors = Vec::new();
 
         const EXPECTED_KEYS: &[&str] = &[
             "type",
@@ -118,40 +220,38 @@ fn parse(it: &mut token_stream::IntoIter) -> Self {
         const REQUIRED_KEYS: &[&str] = &["type", "name", "license"];
         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,
-            };
+        for field in fields {
+            let key = field.key();
 
             if seen_keys.contains(&key) {
-                panic!("Duplicated key \"{key}\". Keys can only be specified once.");
+                errors.push(Error::new_spanned(
+                    &key,
+                    format!(r#"duplicated key "{key}". Keys can only be specified once."#),
+                ));
+                continue;
             }
+            seen_keys.push(key);
 
-            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)),
-                _ => panic!("Unknown key \"{key}\". Valid keys are: {EXPECTED_KEYS:?}."),
+            match field {
+                ModInfoField::Type(_, _, ty) => info.type_ = ty.to_string(),
+                ModInfoField::Name(_, _, name) => info.name = name.value(),
+                ModInfoField::Authors(_, _, _, list) => {
+                    info.authors = Some(list.into_iter().map(|x| x.value()).collect())
+                }
+                ModInfoField::Description(_, _, desc) => info.description = Some(desc.value()),
+                ModInfoField::License(_, _, license) => info.license = license.value(),
+                ModInfoField::Alias(_, _, _, list) => {
+                    info.alias = Some(list.into_iter().map(|x| x.value()).collect())
+                }
+                ModInfoField::Firmware(_, _, _, list) => {
+                    info.firmware = Some(list.into_iter().map(|x| x.value()).collect())
+                }
             }
-
-            assert_eq!(expect_punct(it), ',');
-
-            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}\".");
+                errors.push(Error::new(span, format!(r#"missing required key "{key}""#)));
             }
         }
 
@@ -163,18 +263,24 @@ 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:?}.");
+            errors.push(Error::new(
+                span,
+                format!(r#"keys are not ordered as expected. Order them like: {ordered_keys:?}."#),
+            ));
+        }
+
+        if let Some(err) = errors.into_iter().reduce(|mut e1, e2| {
+            e1.combine(e2);
+            e1
+        }) {
+            return Err(err);
         }
 
-        info
+        Ok(info)
     }
 }
 
-pub(crate) fn module(ts: TokenStream) -> TokenStream {
-    let mut it = ts.into_iter();
-
-    let info = ModuleInfo::parse(&mut it);
-
+pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
     // Rust does not allow hyphens in identifiers, use underscore instead.
     let ident = info.name.replace('-', "_");
     let mut modinfo = ModInfoBuilder::new(ident.as_ref());
@@ -203,7 +309,7 @@ pub(crate) fn module(ts: TokenStream) -> TokenStream {
         std::env::var("RUST_MODFILE").expect("Unable to fetch RUST_MODFILE environmental variable");
     modinfo.emit_only_builtin("file", &file);
 
-    format!(
+    Ok(format!(
         "
             /// The module name.
             ///
@@ -377,5 +483,5 @@ unsafe fn __exit() {{
         initcall_section = ".initcall6.init"
     )
     .parse()
-    .expect("Error parsing formatted string into token stream.")
+    .expect("Error parsing formatted string into token stream."))
 }
-- 
2.51.2


  parent reply	other threads:[~2025-12-11 19:29 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-12-11 18:56 [PATCH 00/11] refactor Rust proc macros with `syn` Gary Guo
2025-12-11 18:56 ` [PATCH 01/11] rust: pin-init: internal: remove proc-macro[2] and quote workarounds Gary Guo
2025-12-11 21:50   ` Christian Schrefl
2025-12-15 13:01     ` Gary Guo
2025-12-16  9:43       ` Benno Lossin
2025-12-11 18:56 ` [PATCH 02/11] rust: macros: use `quote!` from vendored crate Gary Guo
2025-12-16  9:47   ` Benno Lossin
2025-12-11 18:56 ` [PATCH 03/11] rust: macros: convert `#[vtable]` macro to use `syn` Gary Guo
2025-12-11 18:56 ` Gary Guo [this message]
2025-12-11 18:56 ` [PATCH 05/11] rust: macros: use `quote!` for `module!` macro Gary Guo
2025-12-11 18:56 ` [PATCH 06/11] rust: macros: convert `#[export]` to use `syn` Gary Guo
2025-12-11 18:56 ` [PATCH 07/11] rust: macros: convert `concat_idents!` " Gary Guo
2025-12-11 18:56 ` [PATCH 08/11] rust: macros: convert `#[kunit_tests]` macro " Gary Guo
2025-12-11 18:56 ` [PATCH 09/11] rust: macros: allow arbitrary types to be used in `module!` macro Gary Guo
2025-12-11 18:56 ` [PATCH 10/11] rust: macros: rearrange `#[doc(hidden)]` " Gary Guo
2025-12-11 18:56 ` [PATCH 11/11] rust: kunit: use `pin_init::zeroed` instead of custom null value Gary Guo
2025-12-16 12:48   ` Benno Lossin

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251211185805.2835633-5-gary@kernel.org \
    --to=gary@kernel.org \
    --cc=a.hindborg@kernel.org \
    --cc=aliceryhl@google.com \
    --cc=bjorn3_gh@protonmail.com \
    --cc=boqun.feng@gmail.com \
    --cc=dakr@kernel.org \
    --cc=gary@garyguo.net \
    --cc=gregkh@linuxfoundation.org \
    --cc=igor.korotin.linux@gmail.com \
    --cc=jose.exposito89@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=lossin@kernel.org \
    --cc=me@kloenk.dev \
    --cc=ojeda@kernel.org \
    --cc=paddymills@proton.me \
    --cc=rust-for-linux@vger.kernel.org \
    --cc=tamird@gmail.com \
    --cc=tmgross@umich.edu \
    --cc=trintaeoitogc@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).