[post] Safe pin projections through view types

Hey all, I wrote an idea I had of how we could potentially use Niko's idea for "view types" to create safe pin projections - and the general challenges of creating safe pin projections in Rust. It's intentionally less researched than some of my other posts, but I figured the idea was interesting enough that it was worth writing down. I hope y'all like it!

One thing I think this misses is that another key part of pin projection is that it's consistent. If you project a field as pinned in one function, you can't project it as unpinned in another, and vice versa.

Perhaps the compiler can be clever enough to infer that all !Unpin types in a view type should remain pinned when viewed.

I think this is unsound because I can have a type implement Unpin even if its fields do not. This means I can freely pin and unpin it, but after projecting it and getting a Pin<&mut NotUnpinField> then it must remain pinned.

You then propose to move the annotation on the self parameter of the functions, but this is equally problematic. Moreover you could also have two functions disagree on which field is pinned, creating problems even if Self is !Unpin.

Another interesting edge case is a pin-unpin-pin sandwich; if &unpin is never leaked outside the safety barrier, you could in theory structurally project the pinning from the outer pinned type to the inner pinned field.

It'd be horrible, awful API design, and painfully unsafe and prone to accidentally becoming plain unsound, but it's theoretically allowed.

1 Like

You're entirely right. A few paragraphs down in the "Unpin" section we go into this in more detail. In order for pin projections to be safe I believe we would need to change the Unpin trait be unsafe.

Whether going through the trouble of making that change is worth the effort is a good question. Because as things are today, we very much don't have a process for making this type of change in the stdlib.

Oh interesting, I hadn't thought of that. I'm familiar with other language sandwiches, so I think I get what you mean. But I'd love to see an example of this if you could share one?

I believe this only applies if a field is !Unpin? If a field is Unpin you can freely move it in and out of Pin containers because it doesn't matter if it's pinned. This is why for example we can call Pin<&mut T: Unpin>::get_mut which we can then pass to mem::swap to get the owned value out of. If Unpin values always had to be contained in a Pin container, I don't believe that method would be sound?

You're right that I didn't write out all the details I should have. That's on me. I was thinking that if we could make pin projections safe [1], then going from a Pin<&mut T> to its fields, any field which is !Unpin would require being pinned in the projection. Failure to do so would result in a compiler error.

error[E0752]: cannot project `timer` as it's not marked `pin`
 --> src/main.rs:8:1
  |
8 | fn poll(self: pin Self { timer, completed }, cx: &mut Context<'_>) -> Poll<Self::Output> {
  |                          ----- help: consider changing this to be pin: `pin timer`


  1. As mentioned in the post: to make pin projecting safe we can't simply use view types. Drop, Unpin, #[repr(packed)] are all involved too. This reply assumes we've figured out all the details to get "safe pin projections". ↩︎

In other words, you're saying that, instead of having an explicit choice anywhere of whether each field should be projected as pin or non-pin, they would just all be projected as pin? (Modulo Unpin, where it doesn't matter.)

If so, I don't see the advantage of linking this to view types or destructuring in function signatures. Why not 'just' extend dot syntax so that given a Pin<&mut MyStruct>, you can write, e.g., &mut pin my_struct.my_field, and it turns into Pin<&mut MyField>? Sure, there could also be a destructuring version, so that you could write something like let MyStruct { pin my_field } = my_struct;. And naturally you could use that in function signatures as well. And if view types are added to the language, there would presumably be some interaction there. But for the most part they seem like orthogonal features.

I see how the two features would be connected if you were using the signatures of methods on a struct as an implicit means to make "project field as pin or non-pin" decisions for the struct itself. In that case, the decision of whether to obtain a field as pinned or non-pinned would be part of the API and affect other code – similar to how, in the original view types proposal, the list of fields that a method borrows would be part of the API and affect callers. So it makes sense that it must be surfaced in the function signature. But then you run into the issue @CAD97 mentioned that the projection must be consistent.

But rather than thinking about pin as "introducing issues into Rust", what if we instead think of pin as "surfacing issues we have in Rust". Pin is a fundamental type in Rust. Transformative even.

Obligatory derail: I still think Pin is the wrong answer to the right question. It was designed to work without making any language changes, and at that goal it succeeded, but at the cost of picking up unnecessary complexity and limitations. Bringing native language support for Pin can at best somewhat reduce that unnecessary complexity, but at worst it could multiply it (something which I get hints of here, e.g. with the suggestion to change Drop::drop signature but have existing impls magically work anyway). !Move seems scary, but in the long run it's less complex than Pin and more powerful; it's not too late.

5 Likes

Pin is not the only type where having better support for projections would be good, another example is Cell -- given &Cell<Struct> it would be nice to be able to obtain &Cell<Field>. Does the approach proposed here scale to also handle that more general case?

5 Likes

And MaybeUninit<T>, and UnsafeCell<T>, and the proposed VolatileCell<T>. And we could create an Unaligned<T> as a safer and more powerful alternative to #[repr(packed)]. (We could create it today, but without field projection I think the ergonomics wouldn't be good enough.)

Depending on how the feature is designed, it might be possible to apply to reference-counted pointers. Imagine if you could go from Rc<Struct> to some kind of MappedRc<Field>, a hypothetical type that represents a +1 to the reference count of the whole Struct but derefs to the Field – similar to what C++ shared_ptr can do.

And perhaps it could even tie into the raw pointer projections in-bounds discussion. If you imagine &T as the 'fundamental pointer type' and all these other types as variant pointer types (regardless of where the & goes: contrast &Cell<T>, Pin<&T>, and Rc<T>), then *const T itself could be seen as another variant pointer type.

There's definitely enormous potential here.

4 Likes

Oh that's interesting; I hadn't thought of that. Perhaps it could be, I'm not sure. I guess I can't immediately think of an example of what you're describing; if you could share one that'd be very helpful!

Turns out there's even a crate for this: cell_project - Rust

1 Like

The key idea was also mentioned in an older thread about generic field projection. History that I know of:

I believe the last point at least touches on the subject mentioned for closing the RFC. If outstanding semantic difficulties of pointer offsetting could be solved by field-projection then that's seems like a clear case for moving from a library to a language feature.

3 Likes