Skip to content

Instantly share code, notes, and snippets.

@samsartor
Last active October 14, 2021 20:36
Show Gist options
  • Save samsartor/cc848e4d10a2dfb2228236a7f1654d4c to your computer and use it in GitHub Desktop.
Save samsartor/cc848e4d10a2dfb2228236a7f1654d4c to your computer and use it in GitHub Desktop.
First-Class Pin Stuff

Goal #1: safe pin projection

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.

Goal #2: self-referential structs

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.

My Hypothesis

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 iff T: Unpin (the resulting reference must be legal)
  • &mut T can cast to &pin T iff T: 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.

Mutable self-borrows

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.

Projection Rules

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:

  1. 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.
  2. 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 a DropPin trait or something to allow custom drop impls again.
  3. I think this should be OK as long as panics are delt with correctly.
  4. 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.

DerefPin

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.

Pin expressions

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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment