Skip to content

self-referential data support#156

Draft
nbdd0121 wants to merge 12 commits into
mainfrom
dev/selfref
Draft

self-referential data support#156
nbdd0121 wants to merge 12 commits into
mainfrom
dev/selfref

Conversation

@nbdd0121

@nbdd0121 nbdd0121 commented Jun 9, 2026

Copy link
Copy Markdown
Member

This adds self-referential data support, making this possible with safe code:

use pin_init::*;

#[pin_data]
struct SelfRef {
    part: &'str str,
    str: String,
}

fn use_self_ref() {
    stack_pin_init!(let foo = pin_init!(SelfRef {
        str: "hello world".to_owned(),
        part: &str[..5],
    }));
}

nbdd0121 added 12 commits June 6, 2026 13:08
Have `__make_init` take `__data` back as an argument. This gives the
`#[pin_data]` macro expansion an opportunity to change the type when
needed. Currently the type expands the same way as the previous
`__ThePinData` type, but this opportunity will be used in the future to
enable self-referencing type support.

Remove the `Clone` and `Copy` implementation for data types which are no
longer needed.

Signed-off-by: Gary Guo <gary@garyguo.net>
Most users would not need to mention the generated projection struct. With
the upcoming self-referential support, there needs to be an additional
projection struct. Instead of give it another publicly visible name, simply
make the name not publicly accessible.

A heavy user user of pin projection outside kernel is for async
programming. I've checked tokio codebase there is a single use case and
that's for enums.

Signed-off-by: Gary Guo <gary@garyguo.net>
As a first step towards adding self-referential data structures in
pin-init, introduce parsing support for needed attributes.

Two attributes are used to denote variance of the field, `#[covariant]` and
`#[not_covariant]`. More types are covariant over lifetimes and therefore
`#[covariant]` is the default if no annotations are found.

`#[borrows]` attribute is used to mark what other fields that the field can
borrow from, and it is also used to mark what types of borrow it is. For
example, `#[borrows(foo)]` indicates that `foo` should be shared borrowed
and `#[borrows(mut foo)]` borrows `foo` mutably. In absence of `#[borrows]`
attribute, `#[pin_data]` would scan the type to see if there're any unbound
lifetime.

Only parsing is included in this commit.

The attribute syntax is inspired by the `ouroboros` crate, although many
different design decisions have been taken.

Link: https://docs.rs/ouroboros [1]
Signed-off-by: Gary Guo <gary@garyguo.net>
Fields that borrow other fields have lifetimes that are within the struct
and these are not part of the struct generics. Therefore, these fields need
to have their lifetime erased.

A naive implementation would be to replace their lifetimes with `'static`.
However, doing so is unsound for multiple reasons:
* Users may directly access such field with field access syntax, and get
  exposed with wrong lifetime;
* Auto trait implementations will cause the struct to be implementing auto
  traits when the type only implements the auto trait for specific
  lifetime. This is similar to how specialization can be unsound if
  specialized on lifetime.

The first issue is easy to solve by simplying wrapping the field in a
struct that blocks direct access. The second issue is more involved. This
is where higher-ranked trait bound comes in. We can define a `ForLt` trait,
and if we have a `F` where `<F as ForLt>::Of<'a>` is the user-provided type
that makes use of 'a that refers to a borrowed field. Then we can use
`for<'a> F::Of<'a>: Send` to constrain that the user-provided type needs to
be `Send` for all lifetimes rather than a specific lifetime before the type
can be proved by the trait resolver to be `Send`.

The above strategies is what underpins the `SelfRef` type, which is used
for all fields that borrows from the other field. The actual implementation
is a bit more complex version so it can handle multiple lifetimes.

Apart from `Send` and `Sync`, we also have an auto-trait `Unpin`.
Obviously, self-referential structs need always be `!Unpin`. This is
enforced by ensuring `SelfRef: !Unpin` and force the generated `Unpin`
implementation never evaluates never satisfy its bound.

Signed-off-by: Gary Guo <gary@garyguo.net>
Check drop order top ensure that usage of lifetime inside self-referential
struct is consistent with the order that the fields will dropped in drop
glue.

First, fields are checked according to their index to ensure that if `a`
borrows from `b`, `b` must outlive `a`. This is simple and produce a very
good diagnostics when misued.

Lifetime bounds can also be indirectly crafted with implied bounds that
make fields well-formed. For example, in this struct

    struct Foo {
        x: &'b &'a (),
        a: String,
        y: PrintOnDrop<&'b str>,
        b: String,
    }

`&'b &'a ()` will imply that `a` outlive `b`, which is inconsistent with
the actual drop order. A more sophisticated method is used to ensure that
this cannot happen.

With this change, the struct itself can now soundly exist. It cannot yet be
initialized with `pin_init!` or projected with `project()` method.

Signed-off-by: Gary Guo <gary@garyguo.net>
We now have the checks to ensure that lifetime relations are what is
expected, we can generate the slot projections in `generate_pin_data` so
self-referential struct can be implemented.

New slot and guard types are defined (`SelfRefSlot` and `SelfRefDropGuard`)
which gives the generated let bindings longer lifetime than the guard
themselves. Higher-ranked trait bound on `__make_init` is used to ensure
that the initialization closure cannot make arbitrary assumptions of those
lifetimes. Currently, this is only implemented for shared borrows.

Signed-off-by: Gary Guo <gary@garyguo.net>
This adds the projection for fields that are shared borrowed or that
borrows other fields but is covariant. Both cases allow a shared reference
to be accessed.

No mutable references can be created for these cases for different reasons:
* For fields that are shared borrowed, aliasing restriction prevents
  creation of mutable reference
* For fields that borrows other fields, their proper type contains field
  lifetimes. These lifetimes cannot be made available in the returned
  `project` struct (because there is no way to represent existential
  lifetime in return position). For covariant types, it is possible to
  shorten these lifetimes to that of `&self`; but doing so requires the
  reference to also be covariant over the pointee type, so we cannot give
  out `&mut` as it is invariant over the pointee.

Due to field-referencing fields being wrapped inside `SelfRef`, the normal
accessor syntax stop working; create accessor methods for these fields
instead.

Signed-off-by: Gary Guo <gary@garyguo.net>
The `project` method needs to perform covariant coercion on covariant
fields, causing them to no longer being mutable. Implement a `with_project`
that does not require covariant coercion by using higher-ranked trait
bounds, thus allow the fields to be assignable inside the callback.

This mechanism can also be used to access non-covariant fields.

Signed-off-by: Gary Guo <gary@garyguo.net>
Allow fields to be mutably referenced by other fields in addition to shared
references. In order for this to be sound, the fields that can be mutably
borrowed are blocked from being accessed via field access syntax or
projection to maintain the aliasing requirements.

Signed-off-by: Gary Guo <gary@garyguo.net>
With the previous patch, non-covariant types can already be accessed inside
projections. As projections are only generated for `Pin<&mut T>`, they're
not accessible otherwise. Add `with_{field_name}` methods so fields can be
accessed using closures with just `&T`.

Signed-off-by: Gary Guo <gary@garyguo.net>
Currently lifetimes are replaced with `ForLt4` trait. This is very general
approach as it uses generic associated type to replace lifetime, so it can
even work when macros are involved. This does cause more generated code,
and does not render in documentation nicely.

Thus, just replace the lifetime in the AST if no macros are involved.

Signed-off-by: Gary Guo <gary@garyguo.net>
Add the outlive relations per field drop order. This allows a single
lifetime to be used when a field potentially borrow from two different
fields, by allowing the longer-living field lifetime to be shortened to a
shorter-living field lifetime.

Signed-off-by: Gary Guo <gary@garyguo.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant