owned this note changed 2 years ago
Published Linked with GitHub

Reading notes: Safe projections through pin types

https://blog.yoshuawuyts.com/safe-pin-projections-through-view-types/

tags: reading-club

Questions


Eric: In first view types for projection example:

// using view types. `timer` is pinned.
fn poll(&mut{ timer, completed } self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { .. }

how do we know timer is pinned but completed is not?

Yosh: good question lol. I'm not sure? I think this was just me handwaving at: "what if that would just work". Later in the post we dive a bit more into details of how that could be made clear via syntax, etc.

Yosh: oh yeah, later on I talk how this could be made to work simply by inferring all !Unpin fields be pinned in the projection. But I don't actually end up advocating for that approach.

Eric: This is answered later in the post.


Tyler: I think this syntax

fn poll(&mut{ pin timer, completed } pin self, cx: &mut Context<'_>) -> Poll<Self::Output> {

would involve still using self, because the view syntax is only "describing the scope of the reference", while something like

fn poll(self: pin Self { pin timer, completed }, cx: &mut Context<'_>) -> Poll<Self::Output> {

would be a destructuring, allowing you to use timer and completed directly in the method body. Is that right?

Yosh: not sure, let's talk about it!

Yosh: I was thinking view types syntax was destructuring. Eric suggested that we use that syntax if we're already destructuring.

Tyler: Makes sense. Maybe view types syntax is destructuring. I like the idea of reusing the existing syntax in any case.

Eric: Can you destructure in an item function?

Nick: Yep

Nick: I think view types is reifying the restriction of which fields a borrow can capture (like fine-grained capture syntax). Explicitly not destructuring

Yosh: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=907cc42af2602d27f2598632f0bf1720 ??

Eric: Fixed version: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e9362200348d1ad4cc1834da3284afbe

Nick: Not too commonly used since you need to repeat the type name, except for tuples which is where I use it.

Tyler: Might be nice if we removed that restriction, e.g. by removing the need for the type annotation.


Yosh: key takeaways I want the async WG to have from this post:

  • Even if we reduce the need for most folks to implement futures by hand, it's still worth improving the ergonomics of it.
  • In order to make pin projections safe, we necessarily require Unpin to be unsafe to implement.

Tyler: How does pin_project handle this with UnsafeUnpin?

Yosh: I believe it implements UnsafeUnpin anytime you use #[pin_project].

Tyler: Marking it unsafe over an edition bound doesn't make sense to me since you wouldn't be able to project into a type that implements it from an old edition. Might need


Tyler: For fixing Drop, what code would the implicitly added where Self: Unpin bound break? There must be some code e.g. in pin-project that knows what it's doing.

impl Drop for Type {
    fn drop(&mut self) {
        let this = unsafe { Pin::new_unchecked(...) }
    }
}

..migrate to..

impl Drop for Type {
    fn drop(self: Pin<&mut Self>) where Self: Unpin {
        let this = unsafe { Pin::new_unchecked(...) }
    }
}

Eric: Easy to update pin_project specifically in a way that's compatible.

Tyler: Yeah, seems unlikely that we have that many impls like this floating around in the wild.

Yosh: My intuition as well.


Eric: Why is pinning #[repr(packed)] a problem?

Yosh: the std::pin docs say so:

Moreover, if your type is #[repr(packed)], the compiler will automatically move fields around to be able to drop them. It might even do that for fields that happen to be sufficiently aligned. As a consequence, you cannot use pinning with a #[repr(packed)] type.

Yosh: Would be nice to represent packed in the type system

Tyler: Compiler can check this as long as pin projection is built in.

Nick: Representing layout details like layout and alignment in the type system is going to be a lot.

Nick: I don't quite understand this doc.. why is e.g. transparently moving a value into a register a problem?

Yosh: When dropping things get reshuffled with #[repr(packed)]. Something else could live there

Eric: The destructor of the moved field would be able to see that it moved.

Tyler: Rust gets to move your values around.. unless they're pinned.

Tyler: Pin seems to be reliably confusing. One reason why could be that it wraps the pointer instead of your value

Nick: The interaction with Unpin too. Pin guarantees your type doesn't move unless it's not Unpin — that sentence is hard to parse.

Tyler: Yes!

Yosh: Ergonomics are a factor. It feels bad to use.

Eric: Seems more complicated than in other languages for some reason. In C# pinning is operational (used for FFI), whereas in Rust it's a declarative thing.

https://docs.microsoft.com/en-us/dotnet/api/system.memory-1.pin?view=net-6.0

(I don't actually have any experience with pinning in C#, I just know it exists.)

Eric: As soon as I've pinned something, the rest of my project is about how to get access to the pinned thing. At least until you realize that pin_project exists.

Yosh: Whole journey of "why do I need pin_project". Hasn't meaningfully improved for new users.

Yosh: mcyoung's talk

Tyler: Evidence for me that we probably need to continue having something like pin

Nick: People want self-referential structs but pinning is not a complete answer. You also can't spell the lifetime of a self-referential struct. Might need to be able to say that all references to your struct are pin, maybe you could do that with a special constructor?

Select a repo