https://blog.yoshuawuyts.com/safe-pin-projections-through-view-types/
reading-club
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
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:
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?