rust: pin-init: internal: pin_data: support tuple struct projections

`#[pin_data]` rejects tuple structs because it assumes every field has a
name, which it uses for the projection field, the `__Unpin` field and the
pin-data accessor.

Identify fields by `syn::Member` instead, so that tuple fields are referred
to by their index in generated field accesses. The names that generated
items still need are derived from the index as `_0`, `_1`, etc.

The projection of a tuple struct is a tuple struct itself, so projected
fields are accessed with the same `.0`, `.1` syntax as on the input
type rather than through synthesised names.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
[ Moved utility code to util.rs as extension trait - Gary ]
Link: https://patch.msgid.link/20260904-tuple-struct-v1-2-72c50bd037fd@garyguo.net
Signed-off-by: Gary Guo <gary@garyguo.net>
This commit is contained in:
Mohamad Alsadhan 2026-09-04 15:12:30 +01:00 committed by Gary Guo
parent c2bc9bd605
commit 1ee699c451
3 changed files with 130 additions and 36 deletions

View File

@ -7,7 +7,8 @@
parse_quote, parse_quote_spanned,
spanned::Spanned,
visit_mut::VisitMut,
Field, Fields, Generics, Ident, Item, PathSegment, Type, TypePath, Visibility, WhereClause,
Field, Fields, Generics, Ident, Index, Item, Member, PathSegment, Type, TypePath, Visibility,
WhereClause,
};
use crate::{
@ -49,6 +50,7 @@ fn to_tokens(&self, tokens: &mut TokenStream) {
struct FieldInfo<'a> {
field: &'a Field,
member: Member,
pinned: bool,
}
@ -129,10 +131,12 @@ pub(crate) fn pin_data(
replacer.visit_generics_mut(&mut struct_.generics);
replacer.visit_fields_mut(&mut struct_.fields);
let is_tuple_struct = matches!(struct_.fields, Fields::Unnamed(_));
let fields: Vec<FieldInfo<'_>> = struct_
.fields
.iter_mut()
.map(|field| {
.enumerate()
.map(|(index, field)| {
let len = field.attrs.len();
field.attrs.retain(|a| !a.path().is_ident("pin"));
let pinned_count = len - field.attrs.len();
@ -144,23 +148,30 @@ pub(crate) fn pin_data(
!field.attrs.iter().any(|a| a.path().is_ident("cfg")),
"cfgs should be all resolved at this point"
);
let member = match &field.ident {
Some(ident) => Member::Named(ident.clone()),
None => Member::Unnamed(Index {
index: index as u32,
span: field.span(),
}),
};
FieldInfo {
field: &*field,
member,
pinned: pinned_count != 0,
}
})
.collect();
for field in &fields {
let ident = field.field.ident.as_ref().unwrap();
if !field.pinned && is_phantom_pinned(&field.field.ty) {
dcx.warn(
field.field,
format!(
"The field `{ident}` of type `PhantomPinned` only has an effect \
"The field {} of type `PhantomPinned` only has an effect \
if it has the `#[pin]` attribute",
field.member.display_name(),
),
);
}
@ -168,8 +179,13 @@ pub(crate) fn pin_data(
let unpin_impl = generate_unpin_impl(&struct_.ident, &struct_.generics, &fields);
let drop_impl = generate_drop_impl(&struct_.ident, &struct_.generics, args);
let projections =
generate_projections(&struct_.vis, &struct_.ident, &struct_.generics, &fields);
let projections = generate_projections(
&struct_.vis,
&struct_.ident,
&struct_.generics,
is_tuple_struct,
&fields,
);
let the_pin_data =
generate_the_pin_data(&struct_.vis, &struct_.ident, &struct_.generics, &fields);
@ -231,7 +247,7 @@ fn generate_unpin_impl(
unreachable!()
};
let pinned_fields = fields.iter().filter(|f| f.pinned).map(|f| {
let ident = f.field.ident.as_ref().unwrap();
let ident = f.member.as_ident();
let ty = &f.field.ty;
quote!(
#ident: #ty
@ -313,6 +329,7 @@ fn generate_projections(
vis: &Visibility,
ident: &Ident,
generics: &Generics,
is_tuple_struct: bool,
fields: &[FieldInfo<'_>],
) -> TokenStream {
let (impl_generics, ty_generics, _) = generics.split_for_impl();
@ -325,28 +342,32 @@ fn generate_projections(
let (fields_decl, fields_proj): (Vec<_>, Vec<_>) = fields
.iter()
.map(|field| {
let Field { vis, ident, ty, .. } = &field.field;
let Field { vis, ty, .. } = &field.field;
let member = &field.member;
// The projection of a tuple struct is a tuple struct itself, so its fields are
// positional and must not be named.
let name = (!is_tuple_struct).then(|| {
let ident = field.member.as_ident();
quote!(#ident:)
});
let ident = ident
.as_ref()
.expect("only structs with named fields are supported");
if field.pinned {
(
quote!(
#vis #ident: ::core::pin::Pin<&'__pin mut #ty>,
#vis #name ::core::pin::Pin<&'__pin mut #ty>,
),
quote!(
// SAFETY: this field is structurally pinned.
#ident: unsafe { ::core::pin::Pin::new_unchecked(&mut #this.#ident) },
#name unsafe { ::core::pin::Pin::new_unchecked(&mut #this.#member) },
),
)
} else {
(
quote!(
#vis #ident: &'__pin mut #ty,
#vis #name &'__pin mut #ty,
),
quote!(
#ident: &mut #this.#ident,
#name &mut #this.#member,
),
)
}
@ -355,24 +376,52 @@ fn generate_projections(
let structurally_pinned_fields_docs = fields
.iter()
.filter(|f| f.pinned)
.map(|f| format!(" - `{}`", f.field.ident.as_ref().unwrap()));
.map(|f| format!(" - {}", f.member.display_name()));
let not_structurally_pinned_fields_docs = fields
.iter()
.filter(|f| !f.pinned)
.map(|f| format!(" - `{}`", f.field.ident.as_ref().unwrap()));
.map(|f| format!(" - {}", f.member.display_name()));
let docs = format!(" Pin-projections of [`{ident}`]");
let (projection_def, projection_init) = if is_tuple_struct {
(
quote! {
#vis struct #projection #generics_with_pin_lt (
#(#fields_decl)*
::core::marker::PhantomData<&'__pin mut ()>,
) #whr;
},
quote! {
#projection(
#(#fields_proj)*
::core::marker::PhantomData,
)
},
)
} else {
(
quote! {
#vis struct #projection #generics_with_pin_lt
#whr
{
#(#fields_decl)*
___pin_phantom_data: ::core::marker::PhantomData<&'__pin mut ()>,
}
},
quote! {
#projection {
#(#fields_proj)*
___pin_phantom_data: ::core::marker::PhantomData,
}
},
)
};
quote! {
#[doc = #docs]
// Allow `non_snake_case` since the same warning will be emitted on
// the struct definition.
#[allow(dead_code, non_snake_case)]
#[doc(hidden)]
#vis struct #projection #generics_with_pin_lt
#whr
{
#(#fields_decl)*
___pin_phantom_data: ::core::marker::PhantomData<&'__pin mut ()>,
}
#projection_def
impl #impl_generics #ident #ty_generics
#whr
@ -390,10 +439,7 @@ impl #impl_generics #ident #ty_generics
) -> #projection #ty_generics_with_pin_lt {
// SAFETY: we only give access to `&mut` for fields not structurally pinned.
let #this = unsafe { ::core::pin::Pin::get_unchecked_mut(self) };
#projection {
#(#fields_proj)*
___pin_phantom_data: ::core::marker::PhantomData,
}
#projection_init
}
}
}
@ -414,11 +460,9 @@ fn generate_the_pin_data(
let field_accessors = fields
.iter()
.map(|f| {
let Field { vis, ident, ty, .. } = f.field;
let field_name = ident
.as_ref()
.expect("only structs with named fields are supported");
let Field { vis, ty, .. } = f.field;
let field_name = f.member.as_ident();
let member = &f.member;
let pin_marker = if f.pinned {
quote!(Pinned)
} else {
@ -443,7 +487,7 @@ fn generate_the_pin_data(
// - If `#pin_marker` is `Pinned`, the corresponding field is structurally
// pinned.
// - Other safety requirements follows the safety requirement.
unsafe { ::pin_init::__internal::Slot::new(&raw mut (*slot).#field_name) }
unsafe { ::pin_init::__internal::Slot::new(&raw mut (*slot).#member) }
}
}
})

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
use proc_macro2::TokenStream;
use syn::Attribute;
use proc_macro2::{Ident, TokenStream};
use quote::format_ident;
use syn::{Attribute, Index, Member};
pub(crate) trait AttrListExt {
fn extract_cfg_attrs(&mut self) -> Vec<TokenStream>;
@ -25,3 +26,29 @@ fn extract_cfg_attrs(&mut self) -> Vec<TokenStream> {
cfg
}
}
pub(crate) trait MemberExt {
/// Returns an identifier for the member.
///
/// Tuple fields have no name of their own, so they are named `_0`, `_1`, ... instead.
fn as_ident(&self) -> Ident;
/// Obtain a display name for the member in diagnostics.
fn display_name(&self) -> String;
}
impl MemberExt for Member {
fn as_ident(&self) -> Ident {
match self {
Member::Named(ident) => ident.clone(),
Member::Unnamed(Index { index, .. }) => format_ident!("_{index}"),
}
}
fn display_name(&self) -> String {
match self {
Member::Named(ident) => format!("`{ident}`"),
Member::Unnamed(Index { index, .. }) => format!("index `{index}`"),
}
}
}

View File

@ -304,6 +304,9 @@
/// This macro enables the use of the [`pin_init!`] macro. When pin-initializing a `struct`,
/// then `#[pin]` directs the type of initializer that is required.
///
/// Tuple structs are supported as well. Their fields have no names, so the generated projection
/// is a tuple struct too and its fields are accessed by index.
///
/// If your `struct` implements `Drop`, then you need to add `PinnedDrop` as arguments to this
/// macro, and change your `Drop` implementation to `PinnedDrop` annotated with
/// `#[`[`macro@pinned_drop`]`]`, since dropping pinned values requires extra care.
@ -327,6 +330,26 @@
/// }
/// ```
///
/// The same as a tuple struct, projected by index:
///
/// ```
/// # #![feature(allocator_api)]
/// # #[path = "../examples/mutex.rs"] mod mutex; use mutex::*;
/// use core::pin::Pin;
/// use pin_init::pin_data;
///
/// enum Command {
/// /* ... */
/// }
///
/// #[pin_data]
/// struct DriverData(#[pin] CMutex<Vec<Command>>, Box<[u8; 1024 * 1024]>);
///
/// fn queue(data: Pin<&mut DriverData>) -> Pin<&mut CMutex<Vec<Command>>> {
/// data.project().0
/// }
/// ```
///
/// ```
/// # #![feature(allocator_api)]
/// # #[path = "../examples/mutex.rs"] mod mutex; use mutex::*;