[RFC PATCH 2/6] rust: pin-init: internal: add syn version of `pinned_drop` proc macro

From: Benno Lossin
Date: Tue Mar 04 2025 - 18:00:00 EST


Implement the `pinned_drop` attribute macro using syn to simplify
parsing by not going through an additional declarative macro.
This not only simplifies the code by a lot, increasing maintainability
and making it easier to implement new features. But also improves the
user experience by improving the error messages one gets when giving
incorrect inputs to the macro.
For example in this piece of code, there is a `drop` function missing:

use pin_init::*;

#[pin_data(PinnedDrop)]
struct Foo {}

#[pinned_drop]
impl PinnedDrop for Foo {}

But this error is communicated very poorly in the declarative macro
version:

error: no rules expected `)`
|
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)

error[E0277]: the trait bound `Foo: PinnedDrop` is not satisfied
|
3 | #[pin_data(PinnedDrop)]
| ^^^^^^^^^^^^^^^^^^^^^^^
| |
| the trait `PinnedDrop` is not implemented for `Foo`
| required by a bound introduced by this call
|
= note: this error originates in the macro `$crate::__pin_data` which comes from the expansion of the attribute macro `pin_data` (in Nightly builds, run with -Z macro-backtrace for more info)

The syn version is much more concise and right to the point:

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

Another example is the following:

use pin_init::*;
use std::pin::Pin;

#[pin_data(PinnedDrop)]
struct Foo {}

#[pinned_drop]
impl PinnedDrop for Foo {
fn drop(self: Pin<&mut Self>) {}

const BAZ: usize = 0;
}

It produces this error in the declarative macro version:

error: no rules expected keyword `const`
|
10 | const BAZ: usize = 0;
| ^^^^^ no rules expected this token in macro call
|
note: while trying to match `)`
--> src/macros.rs
|
| ),
| ^

error[E0277]: the trait bound `Foo: PinnedDrop` is not satisfied
|
3 | #[pin_data(PinnedDrop)]
| ^^^^^^^^^^^^^^^^^^^^^^^
| |
| the trait `PinnedDrop` is not implemented for `Foo`
| required by a bound introduced by this call
|
= note: this error originates in the macro `$crate::__pin_data` which comes from the expansion of the attribute macro `pin_data` (in Nightly builds, run with -Z macro-backtrace for more info)

In the syn version, we get instead:

error[E0438]: const `BAZ` is not a member of trait `pinned_init::PinnedDrop`
|
11 | const BAZ: usize = 0;
| ^^^^^^^^^^^^^^^^^^^^^ not a member of trait `pinned_init::PinnedDrop`

The syn version is only enabled in the user-space version and disabled
in the kernel until syn becomes available there.

Signed-off-by: Benno Lossin <benno.lossin@xxxxxxxxx>
---
rust/pin-init/internal/src/lib.rs | 4 +
rust/pin-init/internal/src/syn_pinned_drop.rs | 77 +++++++++++++++++++
2 files changed, 81 insertions(+)
create mode 100644 rust/pin-init/internal/src/syn_pinned_drop.rs

diff --git a/rust/pin-init/internal/src/lib.rs b/rust/pin-init/internal/src/lib.rs
index 5019efe22662..761fbf8b9c68 100644
--- a/rust/pin-init/internal/src/lib.rs
+++ b/rust/pin-init/internal/src/lib.rs
@@ -29,10 +29,14 @@

mod helpers;
mod pin_data;
+#[cfg(kernel)]
mod pinned_drop;
#[cfg(kernel)]
mod zeroable;

+#[cfg(not(kernel))]
+#[path = "syn_pinned_drop.rs"]
+mod pinned_drop;
#[cfg(not(kernel))]
#[path = "syn_zeroable.rs"]
mod zeroable;
diff --git a/rust/pin-init/internal/src/syn_pinned_drop.rs b/rust/pin-init/internal/src/syn_pinned_drop.rs
new file mode 100644
index 000000000000..1e70ee29adba
--- /dev/null
+++ b/rust/pin-init/internal/src/syn_pinned_drop.rs
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{
+ parse_macro_input, parse_quote, spanned::Spanned, Error, ImplItem, ImplItemFn, ItemImpl,
+ Result, Token,
+};
+
+pub(crate) fn pinned_drop(
+ args: proc_macro::TokenStream,
+ input: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+ parse_macro_input!(args as syn::parse::Nothing);
+ do_impl(parse_macro_input!(input as ItemImpl))
+ .unwrap_or_else(|e| e.into_compile_error())
+ .into()
+}
+
+fn do_impl(mut input: ItemImpl) -> Result<TokenStream> {
+ let Some((_, path, _)) = &mut input.trait_ else {
+ return Err(Error::new_spanned(
+ input,
+ "expected an `impl` block implementing `PinnedDrop`",
+ ));
+ };
+ if !is_pinned_drop(path) {
+ return Err(Error::new_spanned(
+ input,
+ "expected an `impl` block implementing `PinnedDrop`",
+ ));
+ }
+ let mut error = None;
+ if let Some(unsafety) = input.unsafety.take() {
+ error = Some(
+ Error::new_spanned(
+ unsafety,
+ "implementing the trait `PinnedDrop` via `#[pinned_drop]` is not unsafe",
+ )
+ .into_compile_error(),
+ );
+ }
+ input.unsafety = Some(Token![unsafe](input.impl_token.span()));
+ if path.segments.len() != 2 {
+ path.segments.insert(0, parse_quote!(pin_init));
+ }
+ path.leading_colon.get_or_insert(Token![::](path.span()));
+ for item in &mut input.items {
+ match item {
+ ImplItem::Fn(ImplItemFn { sig, .. }) if sig.ident == "drop" => {
+ sig.inputs
+ .push(parse_quote!(_: ::pin_init::__internal::OnlyCallFromDrop));
+ }
+ _ => {}
+ }
+ }
+ Ok(quote! {
+ #error
+ #input
+ })
+}
+
+fn is_pinned_drop(path: &syn::Path) -> bool {
+ if path.segments.len() > 2 {
+ return false;
+ }
+ // If there is a `::`, then the path needs to be `::pin_init::PinnedDrop`.
+ if path.leading_colon.is_some() && path.segments.len() != 2 {
+ return false;
+ }
+ for (actual, expected) in path.segments.iter().rev().zip(["PinnedDrop", "pin_init"]) {
+ if actual.ident != expected {
+ return false;
+ }
+ }
+ true
+}
--
2.48.1