Discussion: The magic pin! macro

If now been trying for a while to make it easy and possible to have a pin on stack/in future which uninitialized, making it initialized in place and then having it behave like a normal pin afterwards. And I'm not the only one, this is wanted and tried by crates like pin_init, pinned_init and owned_pin. However, when I thought about that, one specific thing won't get out of my mind: Essentially, in the most simple form, I want an API like this:

let a = uninit_pin!(); // a: Pin<&mut MaybeUninit<i32>>
let a = a.init(42);    // a: Pin<&mut i32>

But this is way more complicated than you might think at first glance.
The pin having different types while being on the same memory position isn't a problem, because it would have the same memory layout, but what is is that the Drop glue isn't actually on the Pin type, it is inside std's pin!() macro. For me, this is a super weird phenomenon, because as far as I know, this would make pin! achieve something which is impossible for any other (at least declarative) macro. Because what pin! essentially does is write outside it's boundaries.
For the case where we aren't in an async context, writing

let a = pin!(42);

essentially composes down to:

let a = 42;
let a = Pin::new_unchecked(&mut a);

Where it writes outside it's callplace and we then also can't name a. And this is HUGE.
While I am here trying to make a pin!(ManuallyDrop::new(...)) and handling the drop in a wrapper in the pin pointer, essentially having a drop of Pin<Handle<i32>> dropping the pin, so that I can freely transmute that handle to whatever drop glue I want to run on that pinned memory, which is unsound because I can just mem::forget that handle and have successfully violated pin guidelines, which is why I am crying for the Forget RFC to get merged, std's pin! macro is just like "nah, we just hide it from the user and nobody can forget it".

So here's a thought of mine: Couldn't we combine the magic of pin! with the ability to initialize in place in a new std item (or someone tells me that this is somehow doable in proc macros ._.):

let a: Pin<&mut i32> = morph_pin!(MaybeUninit::uninit(), initializer);

fn initializer<'brand>(pin: MorphPin<'brand, MaybeUninit<i32>>) -> MorphPin<'brand, i32> {
    pin.write(42)
}

which would then lower to this:

let a = ManuallyDrop::new(MaybeUninit::uninit());
let a = MorphPin::new(&mut a); // this handles the drop code - currently as MaybeUninit<i32>
let a = initializer(a); // now drop is handled as i32
let a = a.as_pin(); // we name-shadow a so that the drop handler can't be forgotten anymore

Which should be completely sound.

And this should also be possible in async context because this should also work:

async fn test() {
    {
        let a = pin!(42);
        one_time().await;
        let _ = a;
    } // a lived over 1 await, but is dropped here.
    one_time().await;
}

What do you think about this? (god I'm so terrible at clearly writing out what I want :c)

Exposing this capability to user code is being discussed at Tracking Issue for `super let` · Issue #139076 · rust-lang/rust · GitHub

1 Like

Funny how I've read that multiple times but still can't remember it ._. thanks

1 Like

Note that the actual pin macro is a bit less magical than this, it just desugars to essentially Pin { inner: &mut { 42 } }. The block right after the &mut forces a move of whatever you give to Pin, and then temporary promotion kicks in and basically produces what you want. There are however two tricky details that make this hard to emulate in general:

  • the temporary promotion kicks in because this is a constructor expression, it won't work with a function call;

  • this requires the type field to be visible to whoever calls the macro, which is apparently not the case for Pin (it would be very unsound otherwise). The stdlib solves this issue by making Pin's field public but perma-unstable, and giving the pin macro powers to access that field.

super let would give an alternative to solve the first issue, but IMO addressing the second issue would be also nice because having macros be able to access internals of the crate defining them comes up fairly often.

3 Likes

Also note Original `pin!()` macro behavior cannot be expressed in Rust 2024 · Issue #138718 · rust-lang/rust · GitHub. If you want to play around with these ideas, you should do it on edition 2021.

2 Likes