* [PATCH 1/4] rust: pin-init: internal: extract utility code to new module
2026-09-04 14:12 [PATCH 0/4] rust: pin-init: support tuple structs Gary Guo
@ 2026-09-04 14:12 ` Gary Guo
2026-09-04 14:12 ` [PATCH 2/4] rust: pin-init: internal: pin_data: support tuple struct projections Gary Guo
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Gary Guo @ 2026-09-04 14:12 UTC (permalink / raw)
To: Benno Lossin, Miguel Ojeda, Boqun Feng, Björn Roy Baron,
Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
Daniel Almeida, Tamir Duberstein, Alexandre Courbot,
Onur Özkan
Cc: linux-kernel, rust-for-linux, Gary Guo
Create a new `util.rs` to host utility code that are generic and can be
shared by multiple macros.
Signed-off-by: Gary Guo <gary@garyguo.net>
---
rust/pin-init/internal/src/lib.rs | 1 +
rust/pin-init/internal/src/pin_data.rs | 17 +++++------------
rust/pin-init/internal/src/util.rs | 27 +++++++++++++++++++++++++++
3 files changed, 33 insertions(+), 12 deletions(-)
diff --git a/rust/pin-init/internal/src/lib.rs b/rust/pin-init/internal/src/lib.rs
index 60d5093f3128..4d8ff86484b6 100644
--- a/rust/pin-init/internal/src/lib.rs
+++ b/rust/pin-init/internal/src/lib.rs
@@ -18,6 +18,7 @@
mod init;
mod pin_data;
mod pinned_drop;
+mod util;
mod zeroable;
#[proc_macro_attribute]
diff --git a/rust/pin-init/internal/src/pin_data.rs b/rust/pin-init/internal/src/pin_data.rs
index ff194d27565e..074bc6b3091a 100644
--- a/rust/pin-init/internal/src/pin_data.rs
+++ b/rust/pin-init/internal/src/pin_data.rs
@@ -10,7 +10,10 @@
Field, Fields, Generics, Ident, Item, PathSegment, Type, TypePath, Visibility, WhereClause,
};
-use crate::diagnostics::{DiagCtxt, ErrorGuaranteed};
+use crate::{
+ diagnostics::{DiagCtxt, ErrorGuaranteed},
+ util::*,
+};
pub(crate) mod kw {
syn::custom_keyword!(PinnedDrop);
@@ -81,21 +84,11 @@ pub(crate) fn pin_data(
//
// We need to perform this after parsing so we can reliably detect field cfgs.
for (field_idx, field) in struct_.fields.iter_mut().enumerate() {
- let cfg: Vec<_> = field
- .attrs
- .iter()
- .filter(|a| a.path().is_ident("cfg"))
- .map(|a| {
- a.parse_args::<TokenStream>()
- .expect("parse as token stream cannot fail")
- })
- .collect();
-
+ let cfg = field.attrs.extract_cfg_attrs();
if cfg.is_empty() {
continue;
}
- field.attrs.retain(|a| !a.path().is_ident("cfg"));
let cfg_true_struct = quote!(#struct_);
let punctuated = match &mut struct_.fields {
diff --git a/rust/pin-init/internal/src/util.rs b/rust/pin-init/internal/src/util.rs
new file mode 100644
index 000000000000..ed18ab7d45e6
--- /dev/null
+++ b/rust/pin-init/internal/src/util.rs
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use proc_macro2::TokenStream;
+use syn::Attribute;
+
+pub(crate) trait AttrListExt {
+ fn extract_cfg_attrs(&mut self) -> Vec<TokenStream>;
+}
+
+impl AttrListExt for Vec<Attribute> {
+ fn extract_cfg_attrs(&mut self) -> Vec<TokenStream> {
+ let cfg: Vec<_> = self
+ .iter()
+ .filter(|a| a.path().is_ident("cfg"))
+ .map(|a| {
+ a.parse_args::<TokenStream>()
+ .expect("parse as token stream cannot fail")
+ })
+ .collect();
+
+ if !cfg.is_empty() {
+ self.retain(|a| !a.path().is_ident("cfg"));
+ }
+
+ cfg
+ }
+}
--
2.54.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH 2/4] rust: pin-init: internal: pin_data: support tuple struct projections
2026-09-04 14:12 [PATCH 0/4] rust: pin-init: support tuple structs Gary Guo
2026-09-04 14:12 ` [PATCH 1/4] rust: pin-init: internal: extract utility code to new module Gary Guo
@ 2026-09-04 14:12 ` Gary Guo
2026-09-04 14:12 ` [PATCH 3/4] rust: pin-init: internal: init: support tuple structs in `[pin_]init!` Gary Guo
2026-09-04 14:12 ` [PATCH 4/4] rust: pin-init: internal: init: support tuple struct constructor syntax Gary Guo
3 siblings, 0 replies; 5+ messages in thread
From: Gary Guo @ 2026-09-04 14:12 UTC (permalink / raw)
To: Benno Lossin, Miguel Ojeda, Boqun Feng, Björn Roy Baron,
Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
Daniel Almeida, Tamir Duberstein, Alexandre Courbot,
Onur Özkan
Cc: linux-kernel, rust-for-linux, Gary Guo, Mohamad Alsadhan
From: Mohamad Alsadhan <mo@sdhn.cc>
`#[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 ]
Signed-off-by: Gary Guo <gary@garyguo.net>
---
rust/pin-init/internal/src/pin_data.rs | 112 +++++++++++++++++++++++----------
rust/pin-init/internal/src/util.rs | 31 ++++++++-
rust/pin-init/src/lib.rs | 23 +++++++
3 files changed, 130 insertions(+), 36 deletions(-)
diff --git a/rust/pin-init/internal/src/pin_data.rs b/rust/pin-init/internal/src/pin_data.rs
index 074bc6b3091a..8cd9bf139567 100644
--- a/rust/pin-init/internal/src/pin_data.rs
+++ b/rust/pin-init/internal/src/pin_data.rs
@@ -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) }
}
}
})
diff --git a/rust/pin-init/internal/src/util.rs b/rust/pin-init/internal/src/util.rs
index ed18ab7d45e6..ed2c78f0658f 100644
--- a/rust/pin-init/internal/src/util.rs
+++ b/rust/pin-init/internal/src/util.rs
@@ -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}`"),
+ }
+ }
+}
diff --git a/rust/pin-init/src/lib.rs b/rust/pin-init/src/lib.rs
index 7600cdbbbf98..bf77b76c43c8 100644
--- a/rust/pin-init/src/lib.rs
+++ b/rust/pin-init/src/lib.rs
@@ -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::*;
--
2.54.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH 3/4] rust: pin-init: internal: init: support tuple structs in `[pin_]init!`
2026-09-04 14:12 [PATCH 0/4] rust: pin-init: support tuple structs Gary Guo
2026-09-04 14:12 ` [PATCH 1/4] rust: pin-init: internal: extract utility code to new module Gary Guo
2026-09-04 14:12 ` [PATCH 2/4] rust: pin-init: internal: pin_data: support tuple struct projections Gary Guo
@ 2026-09-04 14:12 ` Gary Guo
2026-09-04 14:12 ` [PATCH 4/4] rust: pin-init: internal: init: support tuple struct constructor syntax Gary Guo
3 siblings, 0 replies; 5+ messages in thread
From: Gary Guo @ 2026-09-04 14:12 UTC (permalink / raw)
To: Benno Lossin, Miguel Ojeda, Boqun Feng, Björn Roy Baron,
Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
Daniel Almeida, Tamir Duberstein, Alexandre Courbot,
Onur Özkan
Cc: linux-kernel, rust-for-linux, Gary Guo, Mohamad Alsadhan
From: Mohamad Alsadhan <mo@sdhn.cc>
Extend the initializer syntax so that a field can be named by an index,
addressing tuple struct fields the same way a struct expression does:
pin_init!(Foo { 0: value, 1 <- initializer })
Tuple fields are not exposed by a `let` binding to the fields after them,
since they have no name to bind; `_0` would shadow a user variable.
Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
[ Fixed incorrect index calculation and cleaned up the code - Gary ]
Signed-off-by: Gary Guo <gary@garyguo.net>
---
rust/pin-init/internal/src/init.rs | 115 +++++++++++++++++++++----------------
rust/pin-init/src/lib.rs | 32 +++++++++--
2 files changed, 95 insertions(+), 52 deletions(-)
diff --git a/rust/pin-init/internal/src/init.rs b/rust/pin-init/internal/src/init.rs
index fd0b5ea4a0a3..5920bb281a50 100644
--- a/rust/pin-init/internal/src/init.rs
+++ b/rust/pin-init/internal/src/init.rs
@@ -8,10 +8,13 @@
parse_quote,
punctuated::Punctuated,
spanned::Spanned,
- token, Attribute, Block, Expr, ExprCall, ExprPath, Ident, Path, Token, Type,
+ token, Attribute, Block, Expr, ExprCall, ExprPath, Ident, LitInt, Member, Path, Token, Type,
};
-use crate::diagnostics::{DiagCtxt, ErrorGuaranteed};
+use crate::{
+ diagnostics::{DiagCtxt, ErrorGuaranteed},
+ util::*,
+};
pub(crate) struct Initializer {
attrs: Vec<InitializerAttribute>,
@@ -36,11 +39,11 @@ struct InitializerField {
enum InitializerKind {
Value {
- ident: Ident,
+ member: Member,
value: Option<(Token![:], Expr)>,
},
Init {
- ident: Ident,
+ member: Member,
_left_arrow_token: Token![<-],
value: Expr,
},
@@ -52,9 +55,9 @@ enum InitializerKind {
}
impl InitializerKind {
- fn ident(&self) -> Option<&Ident> {
+ fn member(&self) -> Option<&Member> {
match self {
- Self::Value { ident, .. } | Self::Init { ident, .. } => Some(ident),
+ Self::Value { member, .. } | Self::Init { member, .. } => Some(member),
Self::Code { .. } => None,
}
}
@@ -229,9 +232,9 @@ fn init_fields(
cfgs
};
- let ident = match kind {
- InitializerKind::Value { ident, .. } => ident,
- InitializerKind::Init { ident, .. } => ident,
+ let member = match kind {
+ InitializerKind::Value { member, .. } => member,
+ InitializerKind::Init { member, .. } => member,
InitializerKind::Code { block, .. } => {
let stmt = &block.stmts;
res.extend(quote! {
@@ -243,27 +246,28 @@ fn init_fields(
continue;
}
};
+ let ident = member.as_ident();
let slot = if pinned {
quote! {
// SAFETY:
// - `slot` is valid and properly aligned.
- // - `make_field_check` checks that `&raw mut (*slot).#ident` is properly aligned.
- // - `make_field_check` prevents `#ident` from being used twice, therefore
- // `(*slot).#ident` is exclusively accessed and has not been initialized.
+ // - `make_field_check` checks that `&raw mut (*slot).#member` is properly aligned.
+ // - `make_field_check` prevents `#member` from being used twice, therefore
+ // `(*slot).#member` is exclusively accessed and has not been initialized.
(unsafe { #data.#ident(#slot) })
}
} else {
quote! {
// For `init!()` macro, everything is unpinned.
// SAFETY:
- // - `&raw mut (*slot).#ident` is valid.
- // - `make_field_check` checks that `&raw mut (*slot).#ident` is properly aligned.
- // - `make_field_check` prevents `#ident` from being used twice, therefore
- // `(*slot).#ident` is exclusively accessed and has not been initialized.
+ // - `&raw mut (*slot).#member` is valid.
+ // - `make_field_check` checks that `&raw mut (*slot).#member` is properly aligned.
+ // - `make_field_check` prevents `#member` from being used twice, therefore
+ // `(*slot).#member` is exclusively accessed and has not been initialized.
(unsafe {
::pin_init::__internal::Slot::<::pin_init::__internal::Unpinned, _>::new(
- &raw mut (*#slot).#ident
+ &raw mut (*#slot).#member
)
})
}
@@ -273,11 +277,11 @@ fn init_fields(
let guard = format_ident!("__{ident}_guard", span = Span::mixed_site());
let init = match kind {
- InitializerKind::Value { ident, value } => {
+ InitializerKind::Value { value, .. } => {
let value = value
.as_ref()
.map(|(_, value)| quote!(#value))
- .unwrap_or_else(|| quote!(#ident));
+ .unwrap_or_else(|| quote!(#member));
quote! {
#(#attrs)*
@@ -294,14 +298,23 @@ fn init_fields(
InitializerKind::Code { .. } => unreachable!(),
};
+ // A tuple field has no name that could be bound here (the `_0` identifiers are considered
+ // implementation detail and not user-facing).
+ let binding = match member {
+ Member::Named(ident) => quote! {
+ #(#cfgs)*
+ // Allow `non_snake_case` since the same warning is going to be reported for the
+ // struct field.
+ #[allow(unused_variables, non_snake_case)]
+ let #ident = #guard.let_binding();
+ },
+ Member::Unnamed(_) => quote!(),
+ };
+
res.extend(quote! {
#init
- #(#cfgs)*
- // Allow `non_snake_case` since the same warning is going to be reported for the struct
- // field.
- #[allow(unused_variables, non_snake_case)]
- let #ident = #guard.let_binding();
+ #binding
});
guards.push(guard);
@@ -326,9 +339,9 @@ fn make_field_check(
) -> TokenStream {
let field_attrs: Vec<_> = fields
.iter()
- .filter_map(|f| f.kind.ident().map(|_| &f.attrs))
+ .filter_map(|f| f.kind.member().map(|_| &f.attrs))
.collect();
- let field_name: Vec<_> = fields.iter().filter_map(|f| f.kind.ident()).collect();
+ let field_name: Vec<_> = fields.iter().filter_map(|f| f.kind.member()).collect();
let zeroing_trailer = match init_kind {
InitKind::Normal => None,
InitKind::Zeroing => Some(quote! {
@@ -376,7 +389,8 @@ fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let lh = content.lookahead1();
if lh.peek(End) || lh.peek(Token![..]) {
break;
- } else if lh.peek(Ident) || lh.peek(Token![_]) || lh.peek(Token![#]) {
+ } else if lh.peek(Ident) || lh.peek(LitInt) || lh.peek(Token![_]) || lh.peek(Token![#])
+ {
fields.push_value(content.parse()?);
let lh = content.lookahead1();
if lh.peek(End) {
@@ -450,31 +464,36 @@ fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
impl Parse for InitializerKind {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let lh = input.lookahead1();
- if lh.peek(Token![_]) {
- Ok(Self::Code {
+ let member = if lh.peek(Token![_]) {
+ return Ok(Self::Code {
_underscore_token: input.parse()?,
_colon_token: input.parse()?,
block: input.parse()?,
+ });
+ } else if lh.peek(Ident) || lh.peek(LitInt) {
+ input.parse::<Member>()?
+ } else {
+ return Err(lh.error());
+ };
+
+ let lh = input.lookahead1();
+ if lh.peek(Token![<-]) {
+ Ok(Self::Init {
+ member,
+ _left_arrow_token: input.parse()?,
+ value: input.parse()?,
+ })
+ } else if lh.peek(Token![:]) {
+ Ok(Self::Value {
+ member,
+ value: Some((input.parse()?, input.parse()?)),
+ })
+ } else if matches!(member, Member::Named(_)) && (lh.peek(Token![,]) || lh.peek(End)) {
+ // Short-hand syntax, available for named fields only.
+ Ok(Self::Value {
+ member,
+ value: None,
})
- } else if lh.peek(Ident) {
- let ident = input.parse()?;
- let lh = input.lookahead1();
- if lh.peek(Token![<-]) {
- Ok(Self::Init {
- ident,
- _left_arrow_token: input.parse()?,
- value: input.parse()?,
- })
- } else if lh.peek(Token![:]) {
- Ok(Self::Value {
- ident,
- value: Some((input.parse()?, input.parse()?)),
- })
- } else if lh.peek(Token![,]) || lh.peek(End) {
- Ok(Self::Value { ident, value: None })
- } else {
- Err(lh.error())
- }
} else {
Err(lh.error())
}
diff --git a/rust/pin-init/src/lib.rs b/rust/pin-init/src/lib.rs
index bf77b76c43c8..f020b383851e 100644
--- a/rust/pin-init/src/lib.rs
+++ b/rust/pin-init/src/lib.rs
@@ -598,7 +598,7 @@ macro_rules! stack_try_pin_init {
};
}
-/// Construct an in-place, fallible pinned initializer for `struct`s.
+/// Construct an in-place, fallible pinned initializer for structs, including tuple structs.
///
/// The error type defaults to [`Infallible`]; if you need a different one, write `? Error` at the
/// end, after the struct initializer.
@@ -632,6 +632,28 @@ macro_rules! stack_try_pin_init {
/// # Box::pin_init(demo()).unwrap();
/// ```
///
+/// The fields of a tuple struct are addressed by their index:
+///
+/// ```rust
+/// # use pin_init::*;
+/// # use core::pin::Pin;
+/// #[pin_data]
+/// struct Pair(usize, Bar);
+///
+/// #[pin_data]
+/// struct Bar {
+/// x: u32,
+/// }
+///
+/// # fn demo() -> impl PinInit<Pair> {
+/// let initializer = pin_init!(Pair {
+/// 0: 42,
+/// 1 <- Bar { x: 64 },
+/// });
+/// # initializer }
+/// # Box::pin_init(demo()).unwrap();
+/// ```
+///
/// Arbitrary Rust expressions can be used to set the value of a variable.
///
/// The fields are initialized in the order that they appear in the initializer. So it is possible
@@ -750,9 +772,11 @@ macro_rules! stack_try_pin_init {
///
/// # Syntax
///
-/// As already mentioned in the examples above, inside of `pin_init!` a `struct` initializer with
-/// the following modifications is expected:
+/// As already mentioned in the examples above, inside of `pin_init!` a struct initializer with the
+/// following modifications is expected:
/// - Fields that you want to initialize in-place have to use `<-` instead of `:`.
+/// - Tuple struct fields are named by their index, as in `0: value` or `0 <- initializer`. They
+/// are not exposed by a `let` binding, since they have no name to bind.
/// - You can use `_: { /* run any user-code here */ },` anywhere where you can place fields in
/// order to run arbitrary code.
/// - In front of the initializer you can write `&this in` to have access to a [`NonNull<Self>`]
@@ -791,7 +815,7 @@ macro_rules! stack_try_pin_init {
/// [`NonNull<Self>`]: core::ptr::NonNull
pub use pin_init_internal::pin_init;
-/// Construct an in-place, fallible initializer for `struct`s.
+/// Construct an in-place, fallible initializer for structs, including tuple structs.
///
/// This macro defaults the error to [`Infallible`]; if you need a different one, write `? Error`
/// at the end, after the struct initializer.
--
2.54.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH 4/4] rust: pin-init: internal: init: support tuple struct constructor syntax
2026-09-04 14:12 [PATCH 0/4] rust: pin-init: support tuple structs Gary Guo
` (2 preceding siblings ...)
2026-09-04 14:12 ` [PATCH 3/4] rust: pin-init: internal: init: support tuple structs in `[pin_]init!` Gary Guo
@ 2026-09-04 14:12 ` Gary Guo
3 siblings, 0 replies; 5+ messages in thread
From: Gary Guo @ 2026-09-04 14:12 UTC (permalink / raw)
To: Benno Lossin, Miguel Ojeda, Boqun Feng, Björn Roy Baron,
Andreas Hindborg, Alice Ryhl, Trevor Gross, Danilo Krummrich,
Daniel Almeida, Tamir Duberstein, Alexandre Courbot,
Onur Özkan
Cc: linux-kernel, rust-for-linux, Gary Guo, Mohamad Alsadhan
From: Mohamad Alsadhan <mo@sdhn.cc>
A tuple struct whose fields are all set to a value reads better written
like a call to its constructor than with the indices spelled out:
pin_init!(Foo(value, value))
Parse the two forms into separate types and rewrite the constructor
arguments into the indexed fields they are shorthand for, so that only the
parser has to know about the second form.
The arguments have no names, so they cannot use `<-`. Parse it anyway and
reject it afterwards, which reports the position of every offending `<-`
rather than stopping at the first one.
`cfg` needs different treatment for tuple constructor syntax. As non-derive
proc macros are invoked before cfg is resolved, the macro cannot know
whether a field survives, and dropping a tuple field renumbers every field
after it. That cannot be expressed by attaching a `cfg` attribute to the
initializer of a single field. Thus, resolve tuple field cfgs up front
instead, by generating two cfg-gated invocations of the macro with one
field resolved in each. This is the approach of commit 5bbf2b2deb94 ("rust:
pin-init: internal: rework how `#[pin_data]` handles cfg"), and it is
linear time because only one of the two branches is ever expanded. Struct
expression syntax do not renumber, so using tuple structs with struct
syntax can keep using the existing attribute-based handling.
Suggested-by: Gary Guo <gary@garyguo.net>
Link: https://github.com/Rust-for-Linux/pin-init/pull/165
Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
[ Pre-expand cfgs for tuple init syntax. Use generics instead of separate
types for normalization - Gary ]
Co-developed-by: Gary Guo <gary@garyguo.net>
Signed-off-by: Gary Guo <gary@garyguo.net>
---
rust/pin-init/internal/src/init.rs | 381 ++++++++++++++++++++++++++++++++++---
rust/pin-init/internal/src/lib.rs | 11 +-
rust/pin-init/src/lib.rs | 16 ++
3 files changed, 383 insertions(+), 25 deletions(-)
diff --git a/rust/pin-init/internal/src/init.rs b/rust/pin-init/internal/src/init.rs
index 5920bb281a50..dc5dfb2ca114 100644
--- a/rust/pin-init/internal/src/init.rs
+++ b/rust/pin-init/internal/src/init.rs
@@ -1,14 +1,15 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
use proc_macro2::{Span, TokenStream};
-use quote::{format_ident, quote};
+use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::{
- braced,
+ braced, parenthesized,
parse::{End, Parse},
parse_quote,
- punctuated::Punctuated,
+ punctuated::{Pair, Punctuated},
spanned::Spanned,
- token, Attribute, Block, Expr, ExprCall, ExprPath, Ident, LitInt, Member, Path, Token, Type,
+ token, Attribute, Block, Expr, ExprCall, ExprPath, Ident, Index, LitInt, Member, Path, Token,
+ Type,
};
use crate::{
@@ -16,14 +17,87 @@
util::*,
};
-pub(crate) struct Initializer {
+pub(crate) struct Initializer<Kind = InitExprKind> {
attrs: Vec<InitializerAttribute>,
this: Option<This>,
+ kind: Kind,
+ error: Option<(Token![?], Type)>,
+}
+
+pub(crate) struct InitExprStruct {
path: Path,
brace_token: token::Brace,
fields: Punctuated<InitializerField, Token![,]>,
rest: Option<(Token![..], Expr)>,
- error: Option<(Token![?], Type)>,
+}
+
+pub(crate) struct InitExprTuple {
+ path: Path,
+ paren_token: token::Paren,
+ fields: Punctuated<InitTupleField, Token![,]>,
+}
+
+pub(crate) enum InitExprKind {
+ Struct(InitExprStruct),
+ Tuple(InitExprTuple),
+}
+
+struct InitTupleField {
+ attrs: Vec<Attribute>,
+ /// `<-` is not valid in constructor syntax; it is parsed anyway so that it can be rejected
+ /// with a proper diagnostic instead of a parse error.
+ left_arrow_token: Option<Token![<-]>,
+ value: Expr,
+}
+
+impl InitExprTuple {
+ fn normalize(self) -> InitExprStruct {
+ let InitExprTuple {
+ path,
+ paren_token,
+ fields,
+ } = self;
+ InitExprStruct {
+ path,
+ brace_token: token::Brace {
+ span: paren_token.span,
+ },
+ fields: fields
+ .into_pairs()
+ .enumerate()
+ .map(|(index, pair)| {
+ let (field, comma) = pair.into_tuple();
+ let span = field.value.span();
+ let field = InitializerField {
+ attrs: field.attrs,
+ kind: InitializerKind::Value {
+ member: Member::Unnamed(Index {
+ index: index.try_into().unwrap(),
+ span,
+ }),
+ value: Some((Token, field.value)),
+ },
+ };
+ Pair::new(field, comma)
+ })
+ .collect(),
+ rest: None,
+ }
+ }
+
+ fn validate(&self, dcx: &mut DiagCtxt) -> Result<(), ErrorGuaranteed> {
+ let mut result = Ok(());
+ for field in &self.fields {
+ if let Some(left_arrow_token) = &field.left_arrow_token {
+ result = Err(dcx.error(
+ left_arrow_token,
+ "`<-` is not supported in tuple constructor syntax; name the fields by index \
+ instead, e.g. `Type { 0 <- initializer, 1: value }`",
+ ));
+ }
+ }
+ result
+ }
}
struct This {
@@ -71,16 +145,103 @@ struct DefaultErrorAttribute {
ty: Box<Type>,
}
-pub(crate) fn expand(
+pub(crate) fn expand_with_cfg(
+ initializer: Initializer,
+ default_error: Option<&'static str>,
+ pinned: bool,
+ dcx: &mut DiagCtxt,
+) -> Result<TokenStream, ErrorGuaranteed> {
+ let initializer = match initializer.kind {
+ InitExprKind::Tuple(expr) => {
+ expr.validate(dcx)?;
+
+ let mut initializer = Initializer {
+ attrs: initializer.attrs,
+ this: initializer.this,
+ kind: expr,
+ error: initializer.error,
+ };
+
+ // Removing a tuple field renumbers every field after it, which cannot be expressed with
+ // a `cfg` attribute on the initializer of a single field. Therefore, resolve tuple
+ // field cfgs before continuing. Struct expression syntax uses explicit numbers, so
+ // there is no need to pre-expand them and we only need to emit their cfgs on generated
+ // code.
+ for (field_idx, field) in initializer.kind.fields.iter_mut().enumerate() {
+ let cfg = field.attrs.extract_cfg_attrs();
+
+ if cfg.is_empty() {
+ continue;
+ }
+
+ let true_initializer = initializer.to_token_stream();
+ initializer.kind.fields = initializer
+ .kind
+ .fields
+ .into_pairs()
+ .enumerate()
+ .filter(|&(index, _)| index != field_idx)
+ .map(|(_, pair)| pair)
+ .collect();
+
+ let false_initializer = &initializer;
+
+ let macro_name = if pinned {
+ quote!(::pin_init::pin_init)
+ } else {
+ quote!(::pin_init::init)
+ };
+
+ // Resolve one field at a time until we've got no more tuple field cfgs.
+ //
+ // This is linear time because macro invocations with false cfg will not be
+ // expanded.
+ return Ok(quote! {
+ {
+ // Use `{}` delimiter here so semicolon is not required, otherwise the
+ // expression becomes unit type.
+ #[cfg(all(#(#cfg,)*))]
+ #macro_name! { #true_initializer }
+
+ #[cfg(not(all(#(#cfg,)*)))]
+ #macro_name! { #false_initializer }
+ }
+ });
+ }
+
+ // No cfgs left, we can normalize the initializer to the struct kind.
+ Initializer {
+ attrs: initializer.attrs,
+ this: initializer.this,
+ kind: initializer.kind.normalize(),
+ error: initializer.error,
+ }
+ }
+
+ InitExprKind::Struct(expr) => Initializer {
+ attrs: initializer.attrs,
+ this: initializer.this,
+ kind: expr,
+ error: initializer.error,
+ },
+ };
+
+ expand(initializer, default_error, pinned, dcx)
+}
+
+fn expand(
Initializer {
attrs,
this,
- path,
- brace_token,
- fields,
- rest,
+ kind:
+ InitExprStruct {
+ path,
+ brace_token,
+ fields,
+ rest,
+ },
error,
- }: Initializer,
+ }: Initializer<InitExprStruct>,
default_error: Option<&'static str>,
pinned: bool,
dcx: &mut DiagCtxt,
@@ -99,7 +260,10 @@ pub(crate) fn expand(
} else if let Some(default_error) = default_error {
syn::parse_str(default_error).unwrap()
} else {
- dcx.error(brace_token.span.close(), "expected `? <type>` after `}`");
+ dcx.error(
+ brace_token.span.close(),
+ "expected `? <type>` after initializer",
+ );
parse_quote!(::core::convert::Infallible)
}
},
@@ -377,11 +541,8 @@ fn make_field_check(
}
}
-impl Parse for Initializer {
- fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
- let attrs = input.call(Attribute::parse_outer)?;
- let this = input.peek(Token![&]).then(|| input.parse()).transpose()?;
- let path = input.parse()?;
+impl InitExprStruct {
+ fn parse_with_path(path: Path, input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let content;
let brace_token = braced!(content in input);
let mut fields = Punctuated::new();
@@ -408,6 +569,51 @@ fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
.peek(Token![..])
.then(|| Ok::<_, syn::Error>((content.parse()?, content.parse()?)))
.transpose()?;
+ Ok(Self {
+ path,
+ brace_token,
+ fields,
+ rest,
+ })
+ }
+}
+
+impl InitExprTuple {
+ fn parse_with_path(path: Path, input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
+ let content;
+ let paren_token = parenthesized!(content in input);
+ let mut fields = Punctuated::new();
+ while !content.is_empty() {
+ fields.push_value(InitTupleField {
+ attrs: content.call(Attribute::parse_outer)?,
+ left_arrow_token: content.parse()?,
+ value: content.parse()?,
+ });
+ if content.is_empty() {
+ break;
+ }
+ fields.push_punct(content.parse()?);
+ }
+ Ok(InitExprTuple {
+ path,
+ paren_token,
+ fields,
+ })
+ }
+}
+
+impl Parse for Initializer {
+ fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
+ let attrs = input.call(Attribute::parse_outer)?;
+ let this = input.peek(Token![&]).then(|| input.parse()).transpose()?;
+ let path = input.parse()?;
+ let kind = if input.peek(token::Brace) {
+ InitExprKind::Struct(InitExprStruct::parse_with_path(path, input)?)
+ } else if input.peek(token::Paren) {
+ InitExprKind::Tuple(InitExprTuple::parse_with_path(path, input)?)
+ } else {
+ return Err(input.error("expected curly braces or parentheses"));
+ };
let error = input
.peek(Token![?])
.then(|| Ok::<_, syn::Error>((input.parse()?, input.parse()?)))
@@ -426,10 +632,7 @@ fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
attrs,
this,
- path,
- brace_token,
- fields,
- rest,
+ kind,
error,
})
}
@@ -499,3 +702,137 @@ fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
}
}
}
+
+impl<Kind: ToTokens> ToTokens for Initializer<Kind> {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self {
+ attrs,
+ this,
+ kind,
+ error,
+ } = self;
+ tokens.append_all(attrs);
+ this.to_tokens(tokens);
+ kind.to_tokens(tokens);
+ if let Some((question, ty)) = error {
+ question.to_tokens(tokens);
+ ty.to_tokens(tokens);
+ }
+ }
+}
+
+impl ToTokens for InitExprKind {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ Self::Struct(init) => init.to_tokens(tokens),
+ Self::Tuple(init) => init.to_tokens(tokens),
+ }
+ }
+}
+
+impl ToTokens for InitExprStruct {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self {
+ path,
+ brace_token,
+ fields,
+ rest,
+ } = self;
+ path.to_tokens(tokens);
+ brace_token.surround(tokens, |tokens| {
+ fields.to_tokens(tokens);
+ if let Some((dotdot, expr)) = rest {
+ dotdot.to_tokens(tokens);
+ expr.to_tokens(tokens);
+ }
+ });
+ }
+}
+
+impl ToTokens for InitExprTuple {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self {
+ path,
+ paren_token,
+ fields,
+ } = self;
+ path.to_tokens(tokens);
+ paren_token.surround(tokens, |tokens| fields.to_tokens(tokens));
+ }
+}
+
+impl ToTokens for InitTupleField {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self {
+ attrs,
+ left_arrow_token,
+ value,
+ } = self;
+ tokens.append_all(attrs);
+ left_arrow_token.to_tokens(tokens);
+ value.to_tokens(tokens);
+ }
+}
+
+impl ToTokens for InitializerAttribute {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ Self::DefaultError(DefaultErrorAttribute { ty }) => {
+ quote!(#[default_error(#ty)]).to_tokens(tokens);
+ }
+ }
+ }
+}
+
+impl ToTokens for This {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self {
+ _and_token,
+ ident,
+ _in_token,
+ } = self;
+ _and_token.to_tokens(tokens);
+ ident.to_tokens(tokens);
+ _in_token.to_tokens(tokens);
+ }
+}
+
+impl ToTokens for InitializerField {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let Self { attrs, kind } = self;
+ tokens.append_all(attrs);
+ kind.to_tokens(tokens);
+ }
+}
+
+impl ToTokens for InitializerKind {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ Self::Value { member, value } => {
+ member.to_tokens(tokens);
+ if let Some((colon, expr)) = value {
+ colon.to_tokens(tokens);
+ expr.to_tokens(tokens);
+ }
+ }
+ Self::Init {
+ member,
+ _left_arrow_token,
+ value,
+ } => {
+ member.to_tokens(tokens);
+ _left_arrow_token.to_tokens(tokens);
+ value.to_tokens(tokens);
+ }
+ Self::Code {
+ _underscore_token,
+ _colon_token,
+ block,
+ } => {
+ _underscore_token.to_tokens(tokens);
+ _colon_token.to_tokens(tokens);
+ block.to_tokens(tokens);
+ }
+ }
+ }
+}
diff --git a/rust/pin-init/internal/src/lib.rs b/rust/pin-init/internal/src/lib.rs
index 4d8ff86484b6..c488019d6250 100644
--- a/rust/pin-init/internal/src/lib.rs
+++ b/rust/pin-init/internal/src/lib.rs
@@ -49,12 +49,17 @@ pub fn maybe_derive_zeroable(input: TokenStream) -> TokenStream {
#[proc_macro]
pub fn init(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input);
- DiagCtxt::with(|dcx| init::expand(input, Some("::core::convert::Infallible"), false, dcx))
- .into()
+ DiagCtxt::with(|dcx| {
+ init::expand_with_cfg(input, Some("::core::convert::Infallible"), false, dcx)
+ })
+ .into()
}
#[proc_macro]
pub fn pin_init(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input);
- DiagCtxt::with(|dcx| init::expand(input, Some("::core::convert::Infallible"), true, dcx)).into()
+ DiagCtxt::with(|dcx| {
+ init::expand_with_cfg(input, Some("::core::convert::Infallible"), true, dcx)
+ })
+ .into()
}
diff --git a/rust/pin-init/src/lib.rs b/rust/pin-init/src/lib.rs
index f020b383851e..4a1368741329 100644
--- a/rust/pin-init/src/lib.rs
+++ b/rust/pin-init/src/lib.rs
@@ -654,6 +654,20 @@ macro_rules! stack_try_pin_init {
/// # Box::pin_init(demo()).unwrap();
/// ```
///
+/// A tuple struct whose fields are all set to a value can also be written like a call to its
+/// constructor:
+///
+/// ```rust
+/// # use pin_init::*;
+/// #[pin_data]
+/// struct Pair(usize, usize);
+///
+/// # fn demo() -> impl PinInit<Pair> {
+/// let initializer = pin_init!(Pair(42, 64));
+/// # initializer }
+/// # Box::pin_init(demo()).unwrap();
+/// ```
+///
/// Arbitrary Rust expressions can be used to set the value of a variable.
///
/// The fields are initialized in the order that they appear in the initializer. So it is possible
@@ -777,6 +791,8 @@ macro_rules! stack_try_pin_init {
/// - Fields that you want to initialize in-place have to use `<-` instead of `:`.
/// - Tuple struct fields are named by their index, as in `0: value` or `0 <- initializer`. They
/// are not exposed by a `let` binding, since they have no name to bind.
+/// - A tuple struct can also be initialized with constructor syntax, as in `Type(value, value)`.
+/// Since its arguments are not named, they cannot use `<-`; write them out by index instead.
/// - You can use `_: { /* run any user-code here */ },` anywhere where you can place fields in
/// order to run arbitrary code.
/// - In front of the initializer you can write `&this in` to have access to a [`NonNull<Self>`]
--
2.54.0
^ permalink raw reply [flat|nested] 5+ messages in thread