Safe stack pinning without macro

I just came up with the idea how to implement safe stack pinning without a macro. It's a little bit more boilerplate than the macro but some people don't like macros and this also avoids questionable trick with shadowing the variable.

/// `&mut T` that drops `T` when it goes out of the scope (similar to `Box`)
// Safety: the creator of this type must not access T after this has been created
// IOW `T` will be dropped so the memory behind it is uninitialized
pub struct StackItem<'a, T: 'a >(&'a mut ManuallyDrop<T>);

impl<'a, T: 'a> Deref for StackItem<'a, T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        self.0
    }
}

impl<'a, T: 'a> DerefMut for StackItem<'a, T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.0
    }
}

impl<'a, T: 'a> Drop for StackItem<'a, T> {
    fn drop(&mut self) {
        // Safety: the creator of this type promises not use the value afterwards
        unsafe { ManuallyDrop::drop(self.0) }
    }
}

pub fn pin<'a, T: 'a>(value: T, place: &'a mut MaybeUninit<T>) -> Pin<StackItem<'a, T>> {
    unsafe {
        *place = MaybeUninit::new(value);
        // Safety: we just initialized the value and `ManuallyDrop` is `repr(tansaprent)` so it has the same layout as `T`, MaybeUninit already doesn't drop the value.
        // This function doesn't allow access to the *initialized* `place` after `Pin<StackItem<_>>` is constructed
        Pin::new_unchecked(StackItem(&mut *place.as_mut_ptr().cast::<ManuallyDrop<T>>()))
    }
}

Usage:

let mut place = MaybeUninit::uninit();
let mut pinned = pin(my_value, &mut place);
// assuming `my_value` is a future
pinned.as_mut().poll(context)

playground

This also serves as stackbox and is very similar except it doesn't need the additional Slot type.

Maybe something to add into core?

That’s unsound. You can circumvent the drop guarantee by mem::forgetting the StackItem<'a, T>.

4 Likes

Then you just leak the value. The value still can not be safely accessed as the only thing you get is MaybeUninit<T> which happens to be initialized and it wouldn't be UB to call assume_init_mut on it but it's still unsafe.

You probably need to re-read the section in the documentation that I linked to.

Concretely, for pinned data you have to maintain the invariant that its memory will not get invalidated or repurposed from the moment it gets pinned until when drop is called. Only once drop returns or panics, the memory may be reused.

Leaking the value doesn’t help. It’s possible to mem::forget the StackItem<'a, T> and afterwards re-use the memory where the underlying MaybeUninit<T> is located for something else (e.g. a new T value… or something entirely unrelated once the function returns, and a different function uses the same stack space… etc), without the destructor of T having been ran first, and that already violates the rule quoted above.

3 Likes

Oh, WTF, why is that requirement there? It doesn't make sense to me.

It makes sense for so-called “intrusive” data structures. A data structure can be efficiently living on stack (thus avoiding more expensive heap allocations) by making sure it knows every single place where a pointer back to itself might be located, and then the destructor makes sure to get rid of all those back-pointers before the stack space might be used for something else. See also the section right before the one I’ve linked previously.

1 Like

OK that's a reason but it seems it's just an additional guarantee bolted on pin that otherwise wouldn't have to be there. It could've been something like MustDrop<Pin<T>>. I believe there's no reason for Future to require MustDrop.

1 Like

I’m not familiar with the (definitiely existant somewhere) discussions about why this became part of Pin, while its main use-case are Futures. I can imagine that either, this property might actually be relevant for async code, too, somehow – or at least considered potentially useful and offered for future compatibility – or I could imagine that perhaps people thought it was an extra property that you rarely want to violate anyways, and the simplicitly of not needing to split up multiple Pin-like things for different purposes was seen as a sufficient advantage.

Yeah, that makes sense

I consider this an anti-feature. One thing I absolutely love about Rust is that things are generally split up and thus composable. Don't need Mutex? Use plain Arc! Don't need Arc? Use plain Mutex. Want to use Arc<Mutex<T>> in a bunch of places without boilerplate? Write a type alias!

Especially with unsafe stuff this may hinder various optimizations or usage in some scenarios.

Splitting it out would make it harder to use such types. Say you want to sleep for 10s using tokio, rather than tokio::time::sleep(Duration::from_secs(10)).await (which uses an intrusively linked list between all Sleep instances). Now you did need to do must_drop!(tokio::time::sleep(Duration::from_secs(10))).await or something like that. Except that must_drop!() is not implementable as the async function itself may get dropped too. So you need unsafe code, which is not ergonomic at all.

Ah, so this is confirmation that my first guess, i.e.

is correct, right?

Oh, that's interesting. Maybe there should be a trait similar to Unpin for it:

// This should be auto trait but that'd break existing `unsaafe` code :(
unsafe trait MayForgetPinned {}

`pub fn pin<'a, T: 'a + MayForgetPinned>(value: T, &'a mut MaybeUninit<T>) -> Pin<StackItem<'a, T>> { /* ... */ }

The generated features coming from async fn would then not have MayForgetPinned if they used !MayForgetPinned internally. Although it probably has some API-compatibility consequences.

Then the restriction would only apply to types without this trait. I guess it's too late for this now and the macro-free pin function probably isn't good enough motivation. But I wouldn't be surprised if someone needed this for something more interesting.

Pin has been created to ship async, not to add general-purpose non-movable types to the language. The drop requirement is there specifically for async runtimes.

No, this is an important quality for futures as well. It may not be necessary for epoll/readiness interfaces, but is absolutely required for uring/completion interfaces.

The basic behavior of uring/completion style async is that I give the OS a pointer, which it may write to at any time, and then I get a notification when the write is finished. The guarantee of Drop is then paramount to safety — it gives a point to wait for the OS to confirm that it's no longer going to write to the pointer I gave it.

Without this guarantee, I can make a new object in the same location and then get it clobbered by the OS finishing its write into the old object that oops isn't actually there anymore.

4 Likes

(NOT A CONTRIBUTION)

First, the history. I developed the first version of Pin, which did not contain the drop guarantee. At the time it was also not a wrapper around a pointer, but two types (I think called Anchor and Pin), once of which was basically Pin<Box<T>> and one of which was basically Pin<&mut T>.

At the Rust all hands in 2018, Taylor Cramer showed me that if we restated the pin guarantee to also guarantee destructors running, it was an adequate interface to support intrusive data structures. I don't know if Taylor had in mind specifically using intrusive data structures for futures synchronization primitives, but we definitely also had the idea that making this new API useful for more things would be better, because it would provide more utility to users and win more buy in. At the time adding the pinning guarantee to futures to support async/await was very controversial with the libraries that had to implement futures by hand, because they didn't fully appreciate how much better async/await with references would be than without.

So, we rewrote the requirement, and then later called the types PinBox and PinMut, and then even later on I realised we could abstract Pin over the pointer type and then make it a "mode" applied to any pointer, rather than specific types. From that point forward we never reconsidered making the drop guarantee a separate type, and Pin stabilised in that form.

Could it have been a separate guarantee? Yes. And there are certainly arguments from elegance for doing so. But if it had, it wouldn't have appeared in the Future API contract, and therefore tokio wouldn't be able to rely on it to use intrusive linked lists in its synchronization primitives. So it's a trade off and I don't regret the trade off we chose. But let me be clear: we added the guarantee and then tokio took advantage of it, it was not required for tokio to work - the tokio team didn't even want us to add pinning at all at first!

We never considered the API in @Kixunil's post before adding this guarantee, instead we had another API that was all safe code and relied on tying lifetimes together in a weird way. Ralf objected to this because he thought it was possibly exploiting a bug in rustc's implementation and not actually sound. So we settled on the macro and never looked back.

Nowadays, I think it would be worth looking adding a let binding modifier (e.g. let pin foo = ...) that does essentially what @Kixunil's post does behind the scenes, but correctly guarantees the destructor runs. And if it were possible to also pin project with a binding modifier, that would be really great. If I could change everything I would make Drop take &pin mut self and make Unpin unsafe and this would all work out really well, but Drop's API was already stabilised by the time Pin was invented.

This is not really true. The drop requirement was not required by async runtimes, and was added to make Pin useful for other things. I think this comes from misremembering our claim that it was not a general-purpose self-referential type, which it isn't (e.g. the pattern in rental is totally different and not supported by pin). But Pin is a modifier to guarantee that the referenced memory won't be invalidated until after the destructor runs, which is indeed more than we needed to desugar async functions, and it can be used for any code that needs that.

This is also not really true. None of the libraries I'm familiar with work this way. One (rio) is just unsound, the others all use ownership passing to avoid this problem rather than pinning and blocking in the destructor. Even with pinning to make it sound, blocking in the destructor is an awful way to handle this because then if you want to cancel interest you've blocked this thread until the operation completes; you basically have made your code sync. Even if Rust had async destructors, you couldn't do something like race two sources or time out an operation, because you have to wait until the operation completes before doing anything. I would consider a library that works this way unfit for purpose.

22 Likes

Wow, this finally made the design click for me! It would be really great to add the first six paragraphs to the documentation for Pin.

1 Like

But isn't there always the problem that you could use let pin foo = ... inside a generator/async function, poll it (which creates the binding) and then leak the whole generator/Future, thus also leaking foo?

The generator/future is also pinned, so it too has to have it's destructor run before the memory can be reused and dropping a generator/future drops all live locals, including foo.

1 Like

I guess I misinterpreted it to mean that let pin foo = ... always guarantees foo's destructor to run. But if that's not the case what advantages does it offer over pin!

1 Like

That is what it guarantees. What bjorn3 is saying is that it achieves that guarantee by making the containing generator !Unpin, so you can't leak it either once it's been polled.