[PATCH 05/12] rust: pin-init: rewrite the `#[pinned_drop]` attribute macro using `syn`

Benno Lossin posted 13 patches 1 month ago
There is a newer version of this series
[PATCH 05/12] rust: pin-init: rewrite the `#[pinned_drop]` attribute macro using `syn`
Posted by Benno Lossin 1 month ago
Rewrite the attribute macro for implementing `PinnedDrop` using `syn`.
Otherwise no functional changes intended aside from improved error
messages on syntactic and semantical errors. For example:

When missing the `drop` function in the implementation, the old error
was:

    error: no rules expected `)`
     --> tests/ui/compile-fail/pinned_drop/no_fn.rs:6:1
      |
    6 | #[pinned_drop]
      | ^^^^^^^^^^^^^^ no rules expected this token in macro call
      |
    note: while trying to match keyword `fn`
     --> src/macros.rs
      |
      |             fn drop($($sig:tt)*) {
      |             ^^
      = note: this error originates in the attribute macro `pinned_drop` (in Nightly builds, run with -Z macro-backtrace for more info)

And the new one is:

    error[E0046]: not all trait items implemented, missing: `drop`
     --> tests/ui/compile-fail/pinned_drop/no_fn.rs:7:1
      |
    7 | impl PinnedDrop for Foo {}
      | ^^^^^^^^^^^^^^^^^^^^^^^ missing `drop` in implementation
      |
      = help: implement the missing item: `fn drop(self: Pin<&mut Self>, _: OnlyCallFromDrop) { todo!() }`

Signed-off-by: Benno Lossin <lossin@kernel.org>
---
 rust/pin-init/internal/src/lib.rs         |  6 +-
 rust/pin-init/internal/src/pinned_drop.rs | 87 ++++++++++++-----------
 rust/pin-init/src/macros.rs               | 28 --------
 3 files changed, 52 insertions(+), 69 deletions(-)

diff --git a/rust/pin-init/internal/src/lib.rs b/rust/pin-init/internal/src/lib.rs
index ec593362c5ac..d0156b82b5d3 100644
--- a/rust/pin-init/internal/src/lib.rs
+++ b/rust/pin-init/internal/src/lib.rs
@@ -25,7 +25,11 @@ pub fn pin_data(inner: TokenStream, item: TokenStream) -> TokenStream {
 
 #[proc_macro_attribute]
 pub fn pinned_drop(args: TokenStream, input: TokenStream) -> TokenStream {
-    pinned_drop::pinned_drop(args.into(), input.into()).into()
+    pinned_drop::pinned_drop(
+        parse_macro_input!(args as _),
+        parse_macro_input!(input as _),
+    )
+    .into()
 }
 
 #[proc_macro_derive(Zeroable)]
diff --git a/rust/pin-init/internal/src/pinned_drop.rs b/rust/pin-init/internal/src/pinned_drop.rs
index cf8cd1c42984..4df2cb9959fb 100644
--- a/rust/pin-init/internal/src/pinned_drop.rs
+++ b/rust/pin-init/internal/src/pinned_drop.rs
@@ -1,49 +1,56 @@
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
-use proc_macro2::{TokenStream, TokenTree};
-use quote::quote;
+use proc_macro2::TokenStream;
+use quote::{quote, quote_spanned};
+use syn::{parse::Nothing, parse_quote, spanned::Spanned, ImplItem, ItemImpl, Token};
 
-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 == "impl"));
-    // Ensure that we are implementing `PinnedDrop`.
-    let mut nesting: usize = 0;
-    let mut pinned_drop_idx = None;
-    for (i, tt) in toks.iter().enumerate() {
-        match tt {
-            TokenTree::Punct(p) if p.as_char() == '<' => {
-                nesting += 1;
+pub(crate) fn pinned_drop(_args: Nothing, mut input: ItemImpl) -> TokenStream {
+    let mut errors = vec![];
+    if let Some(unsafety) = input.unsafety {
+        errors.push(quote_spanned! {unsafety.span=>
+            ::core::compile_error!("implementing `PinnedDrop` is safe");
+        });
+    }
+    input.unsafety = Some(Token![unsafe](input.impl_token.span));
+    match &mut input.trait_ {
+        Some((not, path, _for)) => {
+            if let Some(not) = not {
+                errors.push(quote_spanned! {not.span=>
+                    ::core::compile_error!("cannot implement `!PinnedDrop`");
+                });
             }
-            TokenTree::Punct(p) if p.as_char() == '>' => {
-                nesting = nesting.checked_sub(1).unwrap();
-                continue;
+            for (seg, expected) in path
+                .segments
+                .iter()
+                .rev()
+                .zip(["PinnedDrop", "pin_init", ""])
+            {
+                if expected.is_empty() || seg.ident != expected {
+                    errors.push(quote_spanned! {seg.span()=>
+                        ::core::compile_error!("bad import path for `PinnedDrop`");
+                    });
+                }
+                if !seg.arguments.is_none() {
+                    errors.push(quote_spanned! {seg.arguments.span()=>
+                        ::core::compile_error!("unexpected arguments for `PinnedDrop` path");
+                    });
+                }
             }
-            _ => {}
-        }
-        if i >= 1 && nesting == 0 {
-            // Found the end of the generics, this should be `PinnedDrop`.
-            assert!(
-                matches!(tt, TokenTree::Ident(i) if i == "PinnedDrop"),
-                "expected 'PinnedDrop', found: '{tt:?}'"
-            );
-            pinned_drop_idx = Some(i);
-            break;
+            *path = parse_quote!(::pin_init::PinnedDrop);
         }
+        None => errors.push(quote_spanned! {input.impl_token.span=>
+            ::core::compile_error!("expected `impl ... PinnedDrop for ...`, got inherent impl");
+        }),
     }
-    let idx = pinned_drop_idx
-        .unwrap_or_else(|| panic!("Expected an `impl` block implementing `PinnedDrop`."));
-    // Fully qualify the `PinnedDrop`, as to avoid any tampering.
-    toks.splice(idx..idx, quote!(::pin_init::));
-    // Take the `{}` body and call the declarative macro.
-    if let Some(TokenTree::Group(last)) = toks.pop() {
-        let last = last.stream();
-        quote!(::pin_init::__pinned_drop! {
-            @impl_sig(#(#toks)*),
-            @impl_body(#last),
-        })
-    } else {
-        TokenStream::from_iter(toks)
+    for item in &mut input.items {
+        if let ImplItem::Fn(fn_item) = item {
+            if fn_item.sig.ident == "drop" {
+                fn_item
+                    .sig
+                    .inputs
+                    .push(parse_quote!(_: ::pin_init::__internal::OnlyCallFromDrop));
+            }
+        }
     }
+    quote!(#(#errors)* #input)
 }
diff --git a/rust/pin-init/src/macros.rs b/rust/pin-init/src/macros.rs
index 53ed5ce860fc..b80c95612fd6 100644
--- a/rust/pin-init/src/macros.rs
+++ b/rust/pin-init/src/macros.rs
@@ -503,34 +503,6 @@
 #[cfg(not(kernel))]
 pub use ::paste::paste;
 
-/// Creates a `unsafe impl<...> PinnedDrop for $type` block.
-///
-/// See [`PinnedDrop`] for more information.
-///
-/// [`PinnedDrop`]: crate::PinnedDrop
-#[doc(hidden)]
-#[macro_export]
-macro_rules! __pinned_drop {
-    (
-        @impl_sig($($impl_sig:tt)*),
-        @impl_body(
-            $(#[$($attr:tt)*])*
-            fn drop($($sig:tt)*) {
-                $($inner:tt)*
-            }
-        ),
-    ) => {
-        // SAFETY: TODO.
-        unsafe $($impl_sig)* {
-            // Inherit all attributes and the type/ident tokens for the signature.
-            $(#[$($attr)*])*
-            fn drop($($sig)*, _: $crate::__internal::OnlyCallFromDrop) {
-                $($inner)*
-            }
-        }
-    }
-}
-
 /// This macro first parses the struct definition such that it separates pinned and not pinned
 /// fields. Afterwards it declares the struct and implement the `PinData` trait safely.
 #[doc(hidden)]
-- 
2.51.2
Re: [PATCH 05/12] rust: pin-init: rewrite the `#[pinned_drop]` attribute macro using `syn`
Posted by Gary Guo 1 month ago
On Thu Jan 8, 2026 at 1:50 PM GMT, Benno Lossin wrote:
> Rewrite the attribute macro for implementing `PinnedDrop` using `syn`.
> Otherwise no functional changes intended aside from improved error
> messages on syntactic and semantical errors. For example:
>
> When missing the `drop` function in the implementation, the old error
> was:
>
>     error: no rules expected `)`
>      --> tests/ui/compile-fail/pinned_drop/no_fn.rs:6:1
>       |
>     6 | #[pinned_drop]
>       | ^^^^^^^^^^^^^^ no rules expected this token in macro call
>       |
>     note: while trying to match keyword `fn`
>      --> src/macros.rs
>       |
>       |             fn drop($($sig:tt)*) {
>       |             ^^
>       = note: this error originates in the attribute macro `pinned_drop` (in Nightly builds, run with -Z macro-backtrace for more info)
>
> And the new one is:
>
>     error[E0046]: not all trait items implemented, missing: `drop`
>      --> tests/ui/compile-fail/pinned_drop/no_fn.rs:7:1
>       |
>     7 | impl PinnedDrop for Foo {}
>       | ^^^^^^^^^^^^^^^^^^^^^^^ missing `drop` in implementation
>       |
>       = help: implement the missing item: `fn drop(self: Pin<&mut Self>, _: OnlyCallFromDrop) { todo!() }`
>
> Signed-off-by: Benno Lossin <lossin@kernel.org>
> ---
>  rust/pin-init/internal/src/lib.rs         |  6 +-
>  rust/pin-init/internal/src/pinned_drop.rs | 87 ++++++++++++-----------
>  rust/pin-init/src/macros.rs               | 28 --------
>  3 files changed, 52 insertions(+), 69 deletions(-)
>
> diff --git a/rust/pin-init/internal/src/lib.rs b/rust/pin-init/internal/src/lib.rs
> index ec593362c5ac..d0156b82b5d3 100644
> --- a/rust/pin-init/internal/src/lib.rs
> +++ b/rust/pin-init/internal/src/lib.rs
> @@ -25,7 +25,11 @@ pub fn pin_data(inner: TokenStream, item: TokenStream) -> TokenStream {
>  
>  #[proc_macro_attribute]
>  pub fn pinned_drop(args: TokenStream, input: TokenStream) -> TokenStream {
> -    pinned_drop::pinned_drop(args.into(), input.into()).into()
> +    pinned_drop::pinned_drop(
> +        parse_macro_input!(args as _),
> +        parse_macro_input!(input as _),
> +    )

`as _` can be removed.

> +    .into()
>  }
>  
>  #[proc_macro_derive(Zeroable)]
> diff --git a/rust/pin-init/internal/src/pinned_drop.rs b/rust/pin-init/internal/src/pinned_drop.rs
> index cf8cd1c42984..4df2cb9959fb 100644
> --- a/rust/pin-init/internal/src/pinned_drop.rs
> +++ b/rust/pin-init/internal/src/pinned_drop.rs
> @@ -1,49 +1,56 @@
>  // SPDX-License-Identifier: Apache-2.0 OR MIT
>  
> -use proc_macro2::{TokenStream, TokenTree};
> -use quote::quote;
> +use proc_macro2::TokenStream;
> +use quote::{quote, quote_spanned};
> +use syn::{parse::Nothing, parse_quote, spanned::Spanned, ImplItem, ItemImpl, Token};
>  
> -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 == "impl"));
> -    // Ensure that we are implementing `PinnedDrop`.
> -    let mut nesting: usize = 0;
> -    let mut pinned_drop_idx = None;
> -    for (i, tt) in toks.iter().enumerate() {
> -        match tt {
> -            TokenTree::Punct(p) if p.as_char() == '<' => {
> -                nesting += 1;
> +pub(crate) fn pinned_drop(_args: Nothing, mut input: ItemImpl) -> TokenStream {
> +    let mut errors = vec![];

Any reason to not make this `Vec<Error>` and use `syn::Error::combine`?

> +    if let Some(unsafety) = input.unsafety {
> +        errors.push(quote_spanned! {unsafety.span=>
> +            ::core::compile_error!("implementing `PinnedDrop` is safe");
> +        });
> +    }
> +    input.unsafety = Some(Token![unsafe](input.impl_token.span));
> +    match &mut input.trait_ {
> +        Some((not, path, _for)) => {
> +            if let Some(not) = not {
> +                errors.push(quote_spanned! {not.span=>
> +                    ::core::compile_error!("cannot implement `!PinnedDrop`");
> +                });
>              }
> -            TokenTree::Punct(p) if p.as_char() == '>' => {
> -                nesting = nesting.checked_sub(1).unwrap();
> -                continue;
> +            for (seg, expected) in path
> +                .segments
> +                .iter()
> +                .rev()
> +                .zip(["PinnedDrop", "pin_init", ""])
> +            {
> +                if expected.is_empty() || seg.ident != expected {
> +                    errors.push(quote_spanned! {seg.span()=>
> +                        ::core::compile_error!("bad import path for `PinnedDrop`");
> +                    });
> +                }
> +                if !seg.arguments.is_none() {
> +                    errors.push(quote_spanned! {seg.arguments.span()=>
> +                        ::core::compile_error!("unexpected arguments for `PinnedDrop` path");
> +                    });
> +                }
>              }
> -            _ => {}
> -        }
> -        if i >= 1 && nesting == 0 {
> -            // Found the end of the generics, this should be `PinnedDrop`.
> -            assert!(
> -                matches!(tt, TokenTree::Ident(i) if i == "PinnedDrop"),
> -                "expected 'PinnedDrop', found: '{tt:?}'"
> -            );
> -            pinned_drop_idx = Some(i);
> -            break;
> +            *path = parse_quote!(::pin_init::PinnedDrop);
>          }
> +        None => errors.push(quote_spanned! {input.impl_token.span=>

nit: this can be `input.impl_token.span.join(input.self_ty.span())`.

Best,
Gary

> +            ::core::compile_error!("expected `impl ... PinnedDrop for ...`, got inherent impl");
> +        }),
>      }
> -    let idx = pinned_drop_idx
> -        .unwrap_or_else(|| panic!("Expected an `impl` block implementing `PinnedDrop`."));
> -    // Fully qualify the `PinnedDrop`, as to avoid any tampering.
> -    toks.splice(idx..idx, quote!(::pin_init::));
> -    // Take the `{}` body and call the declarative macro.
> -    if let Some(TokenTree::Group(last)) = toks.pop() {
> -        let last = last.stream();
> -        quote!(::pin_init::__pinned_drop! {
> -            @impl_sig(#(#toks)*),
> -            @impl_body(#last),
> -        })
> -    } else {
> -        TokenStream::from_iter(toks)
> +    for item in &mut input.items {
> +        if let ImplItem::Fn(fn_item) = item {
> +            if fn_item.sig.ident == "drop" {
> +                fn_item
> +                    .sig
> +                    .inputs
> +                    .push(parse_quote!(_: ::pin_init::__internal::OnlyCallFromDrop));
> +            }
> +        }
>      }
> +    quote!(#(#errors)* #input)
>  }
Re: [PATCH 05/12] rust: pin-init: rewrite the `#[pinned_drop]` attribute macro using `syn`
Posted by Benno Lossin 1 month ago
On Fri Jan 9, 2026 at 1:12 PM CET, Gary Guo wrote:
> On Thu Jan 8, 2026 at 1:50 PM GMT, Benno Lossin wrote:
>> diff --git a/rust/pin-init/internal/src/pinned_drop.rs b/rust/pin-init/internal/src/pinned_drop.rs
>> index cf8cd1c42984..4df2cb9959fb 100644
>> --- a/rust/pin-init/internal/src/pinned_drop.rs
>> +++ b/rust/pin-init/internal/src/pinned_drop.rs
>> @@ -1,49 +1,56 @@
>>  // SPDX-License-Identifier: Apache-2.0 OR MIT
>>  
>> -use proc_macro2::{TokenStream, TokenTree};
>> -use quote::quote;
>> +use proc_macro2::TokenStream;
>> +use quote::{quote, quote_spanned};
>> +use syn::{parse::Nothing, parse_quote, spanned::Spanned, ImplItem, ItemImpl, Token};
>>  
>> -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 == "impl"));
>> -    // Ensure that we are implementing `PinnedDrop`.
>> -    let mut nesting: usize = 0;
>> -    let mut pinned_drop_idx = None;
>> -    for (i, tt) in toks.iter().enumerate() {
>> -        match tt {
>> -            TokenTree::Punct(p) if p.as_char() == '<' => {
>> -                nesting += 1;
>> +pub(crate) fn pinned_drop(_args: Nothing, mut input: ItemImpl) -> TokenStream {
>> +    let mut errors = vec![];
>
> Any reason to not make this `Vec<Error>` and use `syn::Error::combine`?

Is there a way to have an "empty" error? I dislike using `Option<Error>`
here... If not I'll just create my own helper type instead.

Cheers,
Benno

>> +    if let Some(unsafety) = input.unsafety {
>> +        errors.push(quote_spanned! {unsafety.span=>
>> +            ::core::compile_error!("implementing `PinnedDrop` is safe");
>> +        });
>> +    }
>> +    input.unsafety = Some(Token![unsafe](input.impl_token.span));
>> +    match &mut input.trait_ {
>> +        Some((not, path, _for)) => {
>> +            if let Some(not) = not {
>> +                errors.push(quote_spanned! {not.span=>
>> +                    ::core::compile_error!("cannot implement `!PinnedDrop`");
>> +                });
Re: [PATCH 05/12] rust: pin-init: rewrite the `#[pinned_drop]` attribute macro using `syn`
Posted by Gary Guo 1 month ago
On Fri Jan 9, 2026 at 3:34 PM GMT, Benno Lossin wrote:
> On Fri Jan 9, 2026 at 1:12 PM CET, Gary Guo wrote:
>> On Thu Jan 8, 2026 at 1:50 PM GMT, Benno Lossin wrote:
>>> diff --git a/rust/pin-init/internal/src/pinned_drop.rs b/rust/pin-init/internal/src/pinned_drop.rs
>>> index cf8cd1c42984..4df2cb9959fb 100644
>>> --- a/rust/pin-init/internal/src/pinned_drop.rs
>>> +++ b/rust/pin-init/internal/src/pinned_drop.rs
>>> @@ -1,49 +1,56 @@
>>>  // SPDX-License-Identifier: Apache-2.0 OR MIT
>>>  
>>> -use proc_macro2::{TokenStream, TokenTree};
>>> -use quote::quote;
>>> +use proc_macro2::TokenStream;
>>> +use quote::{quote, quote_spanned};
>>> +use syn::{parse::Nothing, parse_quote, spanned::Spanned, ImplItem, ItemImpl, Token};
>>>  
>>> -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 == "impl"));
>>> -    // Ensure that we are implementing `PinnedDrop`.
>>> -    let mut nesting: usize = 0;
>>> -    let mut pinned_drop_idx = None;
>>> -    for (i, tt) in toks.iter().enumerate() {
>>> -        match tt {
>>> -            TokenTree::Punct(p) if p.as_char() == '<' => {
>>> -                nesting += 1;
>>> +pub(crate) fn pinned_drop(_args: Nothing, mut input: ItemImpl) -> TokenStream {
>>> +    let mut errors = vec![];
>>
>> Any reason to not make this `Vec<Error>` and use `syn::Error::combine`?
>
> Is there a way to have an "empty" error? I dislike using `Option<Error>`
> here... If not I'll just create my own helper type instead.

Why do you need `empty` error?

You can turn a `Vec<Error>` into errors by

    errors.into_iter().reduce(|mut acc, e| { acc.combine(e); acc })

If you want helpers, this would be my preferred approach (similar to how rustc
handles things):

    struct DiagCtxt {
        errors: Vec<Error>,
    }

    struct ErrorGuaranteed;

    impl DiagCtxt {
        fn emit(err: Error) -> ErrorGuaranteed {
            self.errors.push(err);
            ErrorGuaranteed
        }

        fn with<R: ToTokens>(f: impl FnOnce(&mut DiagCtxt) -> Result<T, ErrorGuaranteed>) -> TokenStream {
            let mut dcx = DiagCtxt { errors: Vec::new() };
            let mut result = match f(&mut dcx) {
                Ok(r) => r.into_token_stream(),
                Err(_) => quote!(),
            };
            for err in dcx.errors {
                result.extend(err.into_token_stream());
            }
            result
        }
    }

(untested)

and now you can

    DiagCtxt::with(|dcx| generate_xxx(dcx));

    // Non-fatal error
    dcx.emit(...)

    // Fatal-error
    Err(dcx.emit(...))?

Best,
Gary

>
> Cheers,
> Benno
>
>>> +    if let Some(unsafety) = input.unsafety {
>>> +        errors.push(quote_spanned! {unsafety.span=>
>>> +            ::core::compile_error!("implementing `PinnedDrop` is safe");
>>> +        });
>>> +    }
>>> +    input.unsafety = Some(Token![unsafe](input.impl_token.span));
>>> +    match &mut input.trait_ {
>>> +        Some((not, path, _for)) => {
>>> +            if let Some(not) = not {
>>> +                errors.push(quote_spanned! {not.span=>
>>> +                    ::core::compile_error!("cannot implement `!PinnedDrop`");
>>> +                });