It should be possible to safely project through pinned user types:
struct Data<F: Future> {
future: F,
output: Option<F::Output>,
}
impl<F: Future> Data<F> {
async fn run(self: Pin<&mut Self>) {
// Project Pin<&mut Self> to Pin<&mut F>
let out = self.future.pin().await;
// Project Pin<&mut Self> -> &mut Option<F::Output>
self.output = Some(out);
}
}
This is currently approximated by the pin_project crate.
It should be possible to safely define, initilize, and mutate self-referential user types:
struct Data {
buffer: Vec<u8>,
slice: &'.buffer [u8],
}
let mut data = Data {
buffer: Vec::new(),
slice: &[], // 'static is long enough
};
// Borrow data with &mut, fields can be accessed like normal
buffer.extend(0..100);
// Data -> &pin Data
let data = data.pin;
// Project &pin Data -> &mut &[u8]
// Project &pin Data -> &pin Vec<u8>
// Cast &pin Vec<u8> -> &Vec<u8>
data.slice = data.buffer.as_slice();
// ERROR!
// Can project &pin Data -> &pin Vec<u8>
// Cannot cast &pin Data -> &mut Vec<u8>: The field is perma-borrowed
data.buffer.clear()
This is currently approximated by the rental crate.
I think we could do that stuff by introducing a new &pin T
reference type, which would behave very similarly to Pin<&mut T>
:
&pin T
can always cast to&T
&pin T
can cast to&mut T
iffT: Unpin
(the resulting reference must be legal)&mut T
can cast to&pin T
iffT: Unpin
&pin T
can be assigned into
But with the additional abilies:
&pin (T,)
can project to&pin T
&pin T
can be produced by auto-ref
For the sake of Goal #1 alone, this feature could be made perma-unstable, and could be implemented without reserving a proper keyword. In that case, pin references would only be exposed through the standard library and through auto-ref:
impl<T: Deref> DerefPin for Pin<T> {
fn deref_pin(&pin Self) -> &pin T::Target { ... }
}
trait IntoPinMut {
fn pin(&pin self) -> Pin<&mut Self>;
}
impl<T> IntoPinMut for T {
...
}
fn projection_example(a: Pin<&mut A>) -> Pin<&mut B> {
// IntoPinMut::pin(&pin derefPin::deref_pin((&mut a) as &pin Pin<&mut A>).my_field)
a.my_field.pin()
}
Shipping goal #2 is more complicated. It would require stabalization of new syntax, a keyword reservation, and would probably need additional rules in polonious. I bet it is where where the bikeshed will really get painted.
Re-writing the self-reference example above to allow mutating the slice gives:
struct Data {
buffer: Vec<u8>,
slice: &'.buffer pin [u8],
}
...
// Project &pin Data -> &mut &[u8]
// Project &pin Data -> &pin Vec<u8>
data.slice = data.buffer.as_pin_slice();
// Project &pin Data -> &pin [u8]
// Cast &pin[u8] to &mut [u8]
data.slice[0] += 1;
It is important to notice that slice must be &pin
rather than &mut
because otherwise the following error would occur:
// ERROR!
// Can project &pin Data -> &pin Vec<u8>
// Cannot cast &pin Data -> &mut Vec<u8>: The field is perma-borrowed
data.slice = data.buffer.as_mut_slice();
Note This error is the same as the one we got when trying to clear the buffer. Rust should never ever allow &mut
references to form unless mutation is really possible.
It is always possible to handle projection safely, since &pin Struct
could aways treat a field access as non-structural and produce an &mut Field
. So the real question is: how to decide which fields are structural? Obviously the user could annotate them manually, as in the rental crate. But I'm hoping it is possible to key off of the Unpin
-ness of the parent struct and of each field in the common case. That is, each field which implements Unpin
should be able to produce &pin
references since they convert freely with &mut
. And each field which does not implement Unpin
will be treated as structural and can produce &pin
references as long the projection checklist is met:
- If the user adds their own
Unpin
impl, then all field acceses will be treated as non-structural by&pin
. Otherwise, this should trivially hopd. - Similar to above, if the user adds their own
Drop
impl, then all the field accesses should suddenly produce&mut Field
. Probably we'd want aDropPin
trait or something to allow custom drop impls again. - I think this should be OK as long as panics are delt with correctly.
- All those move operations require
&mut T
, which isn't given out for&pin T where T: Unpin
.
The main reason to start an inititave process would be to make this air-tight.
This trait is a little bit funky and I'm not remotely sure about it. The real goal is to allow Pin<T>
to convert to &pin T::Target
silently. But I think it may also be possible to implement directly on types like Box
, which could allow users to call Box::new
instead of Box::pin
in most cases? That might become really important if pin
gets reserved as a keyword like I'm showing here. The silent conversion works because Pin<&mut T>
can be auto-refed to &mut Pin<&mut T>
which can in-turn be converted to &pin Pin<&mut T>
before getting derefed into &pin T::Target
.
IDK about this one because pin_mut!()
honestly works OK. But if Rust reserves a keyword and everything, I feel like we might as well add an expression to stack-pin things. And obviously it should be postfix since that best maximizes the meme quality. If we also add FnPin
(oh boy, yield closure time!) then it would make sence to use that keyword as a capture modifier as well:
let my_future = async { ... };
pin move |ctx| {
// automatically pin-projects the capture!
my_future.pin().poll(ctx)
}