[Pre-RFC] `field_projection`

That behavior is different. Doesn't &x.y resolve to the value of y, not a reference to it?

I meant a type that can protect multiple fields, but chained projection is also something we should consider, and isn't really addressed yet. Does projection automatically project to the deepest layer?

My trait way would still be safe to use, just not safe to implement. (And maybe I wasn't clear, it would still use the nice field projection syntax, just would require that trait to protect into Pinned !Unpin types).

My point is that it's a fundamentally unsafe thing to implement, so care must be taken, and so it should be an unsafe trait.

I have no doubt people will continue to use the macros, but with the std types instead.

2 Likes

I would not say that the behavior is different:

struct MyStruct {
    foo: Foo,
    bar: usize,
}
struct Foo {
    count: usize,
}

mystruct is of type MyStruct/&mut MyStruct field access works like this:

expression type
mystruct.foo Foo
&mystruct.foo &Foo
&mut mystruct.foo.count &mut usize

when mystruct is of type &mut MaybeUninit<MyStruct> I propose to allow this:

expression type
mystruct.foo MaybeUninit<Foo>
&mystruct.foo &MaybeUninit<Foo>
&mut mystruct.foo.count &mut MaybeUninit<usize>

To me the two behaviors are identical.

What is this?

$a.field is just a place expression. $a can already be any other place expression, so it composes automatically.

But the proposal is about safely implementing pin projection on your own types.

What is your objection against the currently proposed strategy of using attributes?

Why is it fundamentally unsafe?

I guess so, but why not improve the situation such that users will not need to call .project() anymore? If we are already creating a language solution, it should be the best solution.

How would general projections be implemented with a trait?

1 Like

You make a good case. May I recommend adding the correspondence table to the RFC content? I think it would be a valuable addition.

I've edited my reply to be more clear.

So the answer to that question is "yes"? Sorry, I'm not very well acquainted with that jargon.

Is there precedent for field attributes like this elsewhere in the language?

Because you can expose parts of your struct that should be Pin<&mut T> as &mut T instead, which could be unsound.

I guess I need to clarify more. Users would rarely if ever call project() manually. The field projection syntax would call it "under the hood".

struct Struct<T, U> {
    pinned: T,
    unpinned: U,
}

struct StructProjection<'s, T, U> {
    pinned: Pin<&'s mut T>,
    unpinned: &'s mut U,
}

unsafe impl<'s, T, U> PinMutProjectable for Struct<T, U> where Self: 's {
    type Projection = StructProjection<'s, T, U>;
    fn project(self: Pin<&mut Self>) -> Self::Projection {
        StructProjection { ... }
    }
}

let ps: Pin<Struct<T, U>>;

let pinned: Pin<&mut T> = &mut ps.pinned;
let unpinned: &mut U = &mut ps.unpinned;

// would desugar to something like
let pinned: Pin<&mut T> = ps.project().pinned;
let unpinned: &mut U = ps.project().unpinned;

And that would only apply to types that want projection into Pin.

Saying that, though, it does sound almost too special-case, huh. I realize that your solution with the attributes is far more general:

I've thought long and hard about this but I can't think of a way to achieve this with traits.

1 Like

Okay, here's how I think the attributes should work.

  • #[field_projecting($T)]
    Use to mark that a #[repr(transparent)] type supports field projection to $T
  • #[inner_projecting($T, selective = $bool)] selective = false by default
    Use to mark that a non-union type supports inner projection to $T. selective = true if the projection should treat different fields of $T differently (unwrapping some or excluding some).
  • #[projection_target]
    Used to mark the field or variant for projection. Only one field or variant can be projected
  • #[when_projected_from($Wrapper, unwrap(field, names), include(other, fields))]
    Use on a type to specify behavior when projected from $Wrapper, if and only if $Wrapper enables selective projection.
    When projected from $Wrapper: fields in unwrap are not wrapped by $Wrapper, fields in include are wrapped by $Wrapper, and all other fields are excluded from projection.
    • Is it actually useful to be able to exclude fields from projection?

Examples:

#[repr(transparent)]
#[field_projecting($T)]
pub union MaybeUninit<T> {
    uninit: (),
    #[projection_target]
    value: ManuallyDrop<T>,
}

#[inner_projecting(T)]
pub enum Option<T> {
    Some(#[projection_target] T),
    None
}

#[inner_projecting(P, selective = true)]
pub struct Pin<P> {
    #[projection_target]
    pointer: P,
}

#[when_projected_from(Pin, unwrap(first), include(fut1, fut2))]
struct RaceFutures<F1, F2> {
    fut1: F1,
    fut2: F2,
    first: bool,
}

I'd definitely vote for unwrap/unpin, as it makes accidentally exposing an unpinned field less likely. For 2, I'm not sure what you mean. Can you explain a little more? Can't you just treat all fields that aren't #[unpin] as #[pin]?

Accidentally exposing an unpin field isn’t a big issue, you will get an error as soon as you try to use it as pinned and realise you need to mark it. There is no unsoundness that can result unless you are using unsafe somewhere else in the type.

1 Like

I do not understand how projecting multiple fields should look like, would work and what benefit that would give other than be confusing.

The answer is yes, it is something we get "for free" when designing an expression.

I would say pin-project is a rather successful experiment using attributes.

There is no unsoundness here. The author of the struct can choose which fields are structurally pinned (Pin<&mut Field>) and which are not (&mut Field). The part that is unsafe, is to keep the same strategy. You are not allowed to produce both a Pin<&mut Field> and &mut Field. But that is impossible with this proposal (using only safe code).

I see. But the users still have to

  1. create the StructProjection and
  2. implement an unsafe trait.

I do not see how users would not just write this:

#[pin_project]
struct Struct<T, U> {
    #[pin]
    pinned: T,
    unpinned: U,
}

let ps: Pin<&mut Struct<T, U>> = ...;
let ps = ps.project();

let pinned: Pin<&mut T> = ps.pinned;
let unpinned: &mut U = ps.unpinned;

It is not only much shorter, but it is also completely safe.

When we implement something in the language we need to improve upon this situation.

Exactly, but thanks for your feedback!

That is not really necessary, because #[field_projecting] are already #[repr(transparent)], so they can only have one field that is not a ZST.

When dealing with #[inner_projecting] one might want to project multiple fields. Also only projecting one variant of an enum sounds weird. If I have the other variant is it going to panic?

I do not like the noise that gets created from that. I have to list all fields again at the beginning of the struct definition. I think that we should either use the current approach of putting the attribute directly on the field, or require a separate macro invokation:

struct RaceFutures<F1, F2> {
    fut1: F1,
    fut2: F2,
    first: bool,
}
project!(Pin<RaceFutures>: unpin(first), pin(fut1, fut2));

Additionally, I think that giving the unwrap/include operation more meaningful names based on the container also helps.

I do not really like the second approach (adding a separate macro invokation), because it also increases the migration friction for someone coming from pin-project.

Yes you can, but in the Drop trait you implement this function:

impl<F1, F2> Drop for RaceFuture<F1, F2> {
    fn drop(&mut self) { // <- note the function signature!
        let _: &mut F1 = &mut self.fut1; // we can access &mut F1!!!
    }
}

That is why structs with pinned fields cannot safely implement Drop. They instead have to implement PinnedDrop:

impl<F1, F2> PinnedDrop for RaceFuture<F1, F2> {
    fn drop(self: Pin<&mut Self>) {
        let _: Pin<&mut F1> = &mut self.fut1; // world is fine.
    }
}

But we need to detect if someone has a pinned field. With #[unpin] this does not work, because for pinning, unwrapping is actually the safer default (nothing can go wrong if you never create a Pin<&mut T>).

How does this relate to Deref, which also provides a similar facility, i.e. a syntactic way to refer to the fields of one type as if they were fields of another? In particular, what happens when a type both implements Deref and also has the #[field_projecting(T)] or #[inner_projecting(T)] attributes:

  • When T == Deref::Target?
  • When T != Deref::Target?

Thanks for bringing this up, I had not considered that interaction!

I would propose to make field projection have higher priority. When you would have ambiguity then if you would like to use Deref::Target, you could just deref first:

#[inner_projecting(T)]
struct Container<T> {
    inner: T,
    count: usize,
}

impl<T> Deref for Container<T> {
    type Target = usize;
    fn deref(&self) -> &Self::Target {
        &self.count
    }
}

struct Foo {
    count: usize,
}

fn get_foo_count(foo: Container<&Foo>) -> Container<&usize> {
    foo.count
}

fn get_outer_count(foo: Container<&Foo>) -> usize {
    *foo
}

fn get_outer_count_explicit(foo: Container<&Foo>) -> usize {
    *<Container<&Foo> as Deref>::deref(&foo)
}

But that could introduce breakage... We will have to see.

Thanks everyone for their feedback!

I have opened the RFC here.

You have a field attribute with no attribute on the struct (unlike any existing proc macros, including pin-project). The name of that attribute is declared somewhere else entirely, even in another crate. How do you prevent or deal with name collisions? Does the attribute have to be imported?

Even if you just consider #[pin] as a special thing just for Pin (unrelated to defining the name in field_projecting), where else in the Rust language as implemented by the compiler (without external crates) is there precedent for a standalone field attribute like this?

If you expose a field that should be pinned as unpinned, then you allow moving out of that field, and potentially invalidating any self-references elsewhere in the struct.

Example:

#![feature(maybe_uninit_uninit_array)]

use std::pin::Pin;
use std::marker::PhantomPinned;
use std::mem::MaybeUninit;

struct NotUnpin {
    inner: u8,
    _pin: PhantomPinned,
}

struct Unmovable {
    data: [MaybeUninit<NotUnpin>; 16],
    len: usize,
    // pointer into `data` or null
    cursor: *mut NotUnpin,
    _pin: PhantomPinned,
}

impl Unmovable {
    fn new() -> Pin<Box<Self>> {
        Box::pin(Unmovable {
            data: MaybeUninit::uninit_array(),
            len: 0,
            cursor: std::ptr::null_mut(),
            _pin: PhantomPinned,
        })
    }
    
    fn push(self: Pin<&mut Self>, item: u8) {
        assert!(self.len < 16);
        let item = NotUnpin {
            inner: item,
            _pin: PhantomPinned
        };
        
        // Safety: we do not move anything out of the mutable reference
        unsafe {
            let this = self.get_unchecked_mut();
        
            this.data[this.len].write(item);
            this.cursor = this.data[this.len].as_mut_ptr();
            this.len += 1;
        }
    }
    
    fn get_item(self: Pin<&Self>) -> Option<&u8> {
        if self.cursor.is_null() {
            None
        } else {
            Some(unsafe { &(*self.cursor).inner })
        }
    }
}

Maybe I'm doing something else wrong here, or have a misunderstanding or how the projection should desugar. But it seems like if you were to forget to mark data as #[pin], you could easily invalidate cursor with safe code:

fn main() {
    let mut u = Unmovable::new();
    
    dbg!(u.as_ref().get_item());
    u.as_mut().push(1);
    u.as_mut().push(2);
    dbg!(u.as_ref().get_item());
    
    {
        let mut data: Pin<&mut [MaybeUninit<_>; 16]> =  
        // this is just the desugaring of `&mut u.as_mut().data`
        // if `data` is marked with `#[pin]`
        unsafe { u.as_mut().map_unchecked_mut(|u| &mut u.data) };
        
        // error[E0594]: cannot assign to data in dereference 
        // of `Pin<&mut [MaybeUninit<NotUnpin>; 16]>`
        // *data = MaybeUninit::uninit_array();
    }
    
    let data: &mut [MaybeUninit<_>; 16] =  
        // this is just the desugaring of `&mut u.as_mut().data`
        // if `data` is NOT marked with `#[pin]`
        unsafe { &mut u.as_mut().get_unchecked_mut().data };
    
    *data = MaybeUninit::uninit_array();
    
    // woops now `cursor` points to uninitialized memory
    dbg!(u.as_ref().get_item()); // creates a reference to uninitalized memory
}

Yeah, on second thought, there would be no way to exclude certain variants from projection, if they contain $T, so the target attribute is unnecessary.

Well, if it's not useful to exclude certain fields from projection (I don't know if it is or not), then you'd only need to list the unwrap fields.

I think having some attribute at an entirely different location in the code, possibly in the standard library or another crate, define a special word just for this purpose will just lead to confusion. I'm sure it also makes implementing this in the compiler far more difficult. It's better to just have a standard general word for it.

I don't like the second approach either, and I certainly don't think it's better than using an attribute on the struct instead.

I still don't understand why you can't just treat

struct RaceFutures<F1, F2> {
    fut1: F1,
    fut2: F2,
    #[unpin]
    first: bool,
}

like

struct RaceFutures<F1, F2> {
    #[pin]
    fut1: F1,
    #[pin]
    fut2: F2,
    first: bool,
}

To detect if there's a pinned field, you just check if a given field is not marked with #[unpin]. Checking for the absence of #[unpin] is the same as checking for #[pin] and vice-versa.

From the RFC:

This proposal has chosen an attribute named #[unpin] for this purpose. It would only be a marker attribute and provide no functionality by itself. It should be located either in the same module so ::core::pin::unpin or at the type itself ::core::pin::Pin::unpin.

I will clarify this in the other section as well (that the $unwrap identifier is exported as a macro there).

Yes that is a problem, but it originates from the unsafe blocks you have written that rely on data not being moved. I am referring to these unsafe blocks:

and

What I am trying to say is: without any other unsafe you cannot do something wrong by forgetting #[pin]/#[unpin].

I still believe the field itself to be the best location.

Because if a field is marked with #[pin] then you are required to implement PinnedDrop (otherwise you could move out of the pinned reference in Drop). But currently this compiles:

struct Foo {
    notunpin: NotUnpinStruct,
}
impl Drop for Foo {
    fn self(&mut self) {
        let x = mem::take(&mut self.notunpin);
    }
}

With the proposed change of adding an implicit #[pin], this would be equivalent to this:

struct Foo {
    #[pin]
    notunpin: NotUnpinStruct,
}
impl Drop for Foo {
    fn self(&mut self) {
        let x = mem::take(&mut self.notunpin);
    }
}

But now the Drop impl would get a compiler error, because it needs to implement PinnedDrop instead.

And what I am saying is that, with existing sound code that uses some unsafe, pin projection enables unsoundness by default, if you go the #[pin] route.

Hell, it enables unsoundness in safe code in a similar case, even if the field in question is marked #[pin]:

#![feature(maybe_uninit_uninit_array)]

use std::pin::Pin;
use std::marker::PhantomPinned;
use std::mem::MaybeUninit;

struct Unmovable {
    data: [MaybeUninit<u8>; 16],
    len: usize,
    // pointer into `data` or null
    cursor: *mut u8,
    _pin: PhantomPinned,
}

impl Unmovable {
    fn new() -> Pin<Box<Self>> {
        Box::pin(Unmovable {
            data: MaybeUninit::uninit_array(),
            len: 0,
            cursor: std::ptr::null_mut(),
            _pin: PhantomPinned,
        })
    }
    
    fn push(self: Pin<&mut Self>, item: u8) {
        assert!(self.len < 16);
        
        // Safety: we do not move anything out of the mutable reference
        unsafe {
            let this = self.get_unchecked_mut();
        
            this.data[this.len].write(item);
            this.cursor = this.data[this.len].as_mut_ptr();
            this.len += 1;
        }
    }
    
    fn get_item(self: Pin<&Self>) -> Option<&u8> {
        if self.cursor.is_null() {
            None
        } else {
            Some(unsafe { &(*self.cursor) })
        }
    }
}

fn main() {
    let mut u = Unmovable::new();
    
    dbg!(u.as_ref().get_item());
    u.as_mut().push(1);
    u.as_mut().push(2);
    dbg!(u.as_ref().get_item());
    
    {
        let mut data: Pin<&mut [MaybeUninit<_>; 16]> =  
            // this is just the desugaring of `&mut u.as_mut().data`
            // if `data` is marked with `#[pin]`
            unsafe { u.as_mut().map_unchecked_mut(|u| &mut u.data) };
        
        *data = MaybeUninit::uninit_array();
    }
    
    // woops now `cursor` points to uninitialized memory
    dbg!(u.as_ref().get_item());
}

Perhaps pin projection should require unsafe? Maybe only when projecting into Unpin fields?

Ah, I finally understand. Why not just disable projection unless PinnedDrop is implemented?

Also, as I understand it, the "pinned drop" problem isn't really special to this, because you should implement a PinnedDrop kind of thing if your type is !Unpin regardless. So it should probably be a requirement to implement PinnedDrop for any !Unpin type anyways.

You are relying on a field that is Unpin to be pinned. That does not work with pin-project and will not work with any other solution to pin projection. The whole point of Unpin is to allow mutable access to types that do not care about being pinned.

In the unsafe blocks you swear to uphold the invariant that data is not moved. You are placing this additional invariant on the entire codebase not just the unsafe blocks. But in main you do move it and experience UB. That is what you signed up for.

Because Unpin is a safe trait, you could implement it for Unmovable and then you could do the moving out without any unsafe. Pin is very tricky. The docs clearly state that this is forbidden though.

That is a good alternative, I will add it! I am not really sure, about it, because it would make Pin/PinnedDrop more special compared to the other options.

Not quiet. You should implement PinnedDrop if your type is !Unpin and your type gets pinned. So as long as no Pin<&mut T> get created of your type PinnedDrop is not needed.

Exactly. data should not be mutably projected under any circumstance, in order to uphold the invariants that the type depends on. In the absence of a field projection feature, this is fine, because there is no way to mutably access fields of Pin<&mut Unmovable> with safe code - this is guaranteed by Pin.

Pin<&mut Unmovable> guarantees that data cannot be moved. The only way for this type to be constructed in safe code is already pinned, and the only way to push or get is on a pinned reference. That is what upholds the invariants. The rest of the codebase doesn't need to worry about upholding the invariant, because it will be enforced by Pin.

There is no way to cause undefined behavior with safe code, unless mutable projection into Unmovable is allowed in safe code via the field projection feature.

You are not able to access fields that are not visible to the current module (I think I did not mention that yet in the RFC).

main clearly accesses the field directly. I suggest moving the struct definition into its own module.

This again shows how difficult pinning actually is. Here is your example implemented with pin-project with only one singular unsafe:

#![feature(maybe_uninit_uninit_array)]

use pin_project::pin_project;

use std::pin::Pin;
use std::marker::PhantomPinned;
use std::mem::MaybeUninit;

#[pin_project]
struct Unmovable {
    #[pin]
    data: [MaybeUninit<u8>; 16],
    len: usize,
    // pointer into `data` or null
    cursor: *mut u8,
    #[pin]
    _pin: PhantomPinned,
}

impl Unmovable {
    fn new() -> Pin<Box<Self>> {
        Box::pin(Unmovable {
            data: MaybeUninit::uninit_array(),
            len: 0,
            cursor: std::ptr::null_mut(),
            _pin: PhantomPinned,
        })
    }
    
    fn push(self: Pin<&mut Self>, item: u8) {
        let mut this = self.project();
        assert!(*this.len < 16);
        this.data[*this.len].write(item);
        *this.cursor = this.data[*this.len].as_mut_ptr();
        *this.len += 1;
    }
    
    fn get_item(self: Pin<&Self>) -> Option<&u8> {
        if self.cursor.is_null() {
            None
        } else {
            // SAFETY: cursor is valid
            Some(unsafe { &(*self.cursor) })
        }
    }
}

fn main() {
    let mut u = Unmovable::new();
    
    dbg!(u.as_ref().get_item());
    u.as_mut().push(1);
    u.as_mut().push(2);
    dbg!(u.as_ref().get_item());
    
    {
        let mut data: Pin<&mut [MaybeUninit<_>; 16]> = u.as_mut().project().data;
        
        *data = MaybeUninit::uninit_array();
    }
    
    // woops now `cursor` points to uninitialized memory
    dbg!(u.as_ref().get_item());
}

When you execute it with miri, it correctly detects the UB that is happening (note that everything is fine until line 63).

That was my understanding from the get go.

Unsoundness shouldn't be possible within the module, either. But if it helps, just imagine that data is a public field. The type is still sound in that case.

Yes, but #[pin_project] is an opt-in thing. They only guarantee it's safe in the absence of other unsafe code. When applying #[pin_project], you must be sure that your type is still sound when projecting all fields (whether with or without Pin wrapping).

However, how you have it, this language feature applies by default, and can allow undefined behavior in safe code for a previously sound type. It doesn't matter whether it's within the same module or a publicly visible field. It allows unsound usage of existing sound types just by existing, because it projects all fields by default.

If instead, you had to opt-in by explicitly enumerating which fields are projectable (or if your type as a whole is projectable - though that is less flexible), it wouldn't be a problem.

If data were !Unpin I would agree.

Adding #[pin] on your type would be opt-in as well. This feature would also have the same safety guarantee.

In the example you gave there is an unsafe block relying on the validity of the pointer cursor. You invalidate the pointer via safe code, but the UB still occurs due to the unsafe block. I do not see how this proposal produces any UB without unsafe code.

I want to take your concern seriously, I have tried to come up with an example that uses no unsafe and also exhibits UB, but was unable to find one. That of course does not mean that there does not exist one. But as the current proposal is almost exactly like pin-project, I doubt that there is hidden unsoundness.

Could you elaborate how this would fix your example?

I demonstrated a similar issue where data was !Unpin, just by forgetting #[pin], which is the default state you prefer.

Also, I'd like to add that preventing the data projection is highly valuable within the module as well. For instance, consider this safe implementation of set_data using field projection:

fn set_data(self: Pin<&mut Self>, data: [MaybeUninit<u8>; 16]) {
    self.data = data;
}

But that is unsound, as I haven't updated cursor. If writing to data required unsafe, I would at least have a hint that I need to do more to uphold the invariants:

fn set_data(self: Pin<&mut Self>, data: [MaybeUninit<u8>; 16]) {
    self.cursor = std::ptr::null_mut(); // the new data at `cursor` could be uninitialized

    unsafe { self.data = data; }
    // or
    unsafe {
        let this_data = &mut self.get_unchecked_mut().data;
        *this_data = data;
    }
}

Are you saying you would require at least one #[pin] for projection to apply at all?

It is impossible to trigger the UB in any usage of the API, unless this feature is present. If I had a fn project_data(Pin<&mut Self>) -> Pin<&mut data>, that would likewise make the type unsound.

The proposal doesn't produce UB, because of course the projection would need to be used to produce UB. What it does is enable UB in safe code for types which are currently sound. It makes existing sound types unsound by automatically adding projection to them.

It would be like if the compiler automatically added that project_data function to Unmovable.

I would exclude the data field from projection. Then the data pointed to by cursor couldn't be modified by safe code.

1 Like

I kinda get the feeling that we are talking past each other. So I will return to the original example:

#![feature(maybe_uninit_uninit_array)]

use std::pin::Pin;
use std::marker::PhantomPinned;
use std::mem::MaybeUninit;

struct Unmovable {
    data: [MaybeUninit<u8>; 16],
    len: usize,
    // pointer into `data` or null
    cursor: *mut u8,
    _pin: PhantomPinned,
}

impl Unmovable {
    fn new() -> Pin<Box<Self>> {
        Box::pin(Unmovable {
            data: MaybeUninit::uninit_array(),
            len: 0,
            cursor: std::ptr::null_mut(),
            _pin: PhantomPinned,
        })
    }
    
    fn push(self: Pin<&mut Self>, item: u8) {
        assert!(self.len < 16);
        
        // Safety: we do not move anything out of the mutable reference
        unsafe {
            let this = self.get_unchecked_mut();
        
            this.data[this.len].write(item);
            this.cursor = this.data[this.len].as_mut_ptr();
            this.len += 1;
        }
    }
    
    fn get_item(self: Pin<&Self>) -> Option<&u8> {
        if self.cursor.is_null() {
            None
        } else {
            // SAFETY: when `cursor` is not null, it points into `data`
            // and is valid for reads (the memory is initialized).
            // **This invariant needs to be upheld not only by `unsafe`
            // code, but safe code as well.**
            Some(unsafe { &(*self.cursor) })
        }
    }
}

fn main() {
    let mut u = Unmovable::new();
    
    dbg!(u.as_ref().get_item());
    u.as_mut().push(1);
    u.as_mut().push(2);
    dbg!(u.as_ref().get_item());
    
    {
        let mut data: Pin<&mut [MaybeUninit<_>; 16]> =  
            // this is just the desugaring of `&mut u.as_mut().data`
            // if `data` is marked with `#[pin]`
            unsafe { u.as_mut().map_unchecked_mut(|u| &mut u.data) };
        
        *data = MaybeUninit::uninit_array();
    }
    
    // woops now `cursor` points to uninitialized memory
    dbg!(u.as_ref().get_item());
}

Note the safety comment on the unsafe block in get_item:

SAFETY: when cursor is not null, it points into data and is valid for reads (the memory is initialized). This invariant needs to be upheld not only by unsafe code, but safe code as well.


The proposed feature does not introduce new unsoundness into this API. As you have demonstrated by the example, there is already the potential for UB.

The only difference between this and the version that uses pin-project/this feature is: the lack of unsafe in the part where you modify the data field.

I am saying that we can attribute the unsoundness to the unsafe block situated in the get_item function. The user has not upheld the invariant. Without this unsafe block, everything would be fine.