Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign up[Stabilization] Pin APIs #55766
Comments
withoutboats
added
the
T-libs
label
Nov 7, 2018
This comment has been minimized.
This comment has been minimized.
|
@rfcbot fcp merge |
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Nov 7, 2018
•
|
Team member @withoutboats has proposed to merge this. The next step is review by the rest of the tagged team members: Concerns:
Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
rfcbot
added
proposed-final-comment-period
disposition-merge
labels
Nov 7, 2018
This comment has been minimized.
This comment has been minimized.
|
Thanks for the detailed writeup here @withoutboats! I've also sort of been historically confused by the various guarantees of In trying to start writing this down though I keep running up against a wall of "what is |
This comment has been minimized.
This comment has been minimized.
|
if i have got this correct, every type that is not self referential (ie: not a generator) is Unpin |
This comment has been minimized.
This comment has been minimized.
|
It's not just self-referentiality, there are some other use-cases for stable memory addresses that How I understand |
This comment has been minimized.
This comment has been minimized.
|
Hm I still don't really understand First off, it's probably helpful to know what types implement I keep trying to summarize or state what I think I understand the guarantees of |
This comment has been minimized.
This comment has been minimized.
|
@alexcrichton Thanks for the questions, I'm sure the pinning APIs can be a bit confusing for people who haven't been part of the group focusing on them.
Unpin is an auto trait like Send or Sync, so most types implement it automatically. Generators and the types of async functions are The explicit impls for pointer types is to make the
Here is the sort of fundamental idea of the pinning APIs. First, given a pointer type
The implication of all of this is that if you construct a So if you implement Moving a pointer type like |
This comment has been minimized.
This comment has been minimized.
This was one of the trickiest parts of the pinning API, and we got it wrong at first. Unpin means "even once something has been put into a pin, it is safe to get a mutable reference to it." There is another trait that exists today that gives you the same access: To actually implement a self-referential type would require unsafe code - in practice, the only self-referential types anyone cares about are those that the compiler generates for you: generators and async function state machines. These explicitly say they don't implement The problem emerges once you have a struct containing one of these anonymous types, like a future combinator. In order go from a For this reason, pin projection is unsafe! In order to perform a pin projection without violating the pinning invariants, you have to guarantee that you never do several things, which I listed in the stabilization proposal. So, the tl;dr: |
This comment has been minimized.
This comment has been minimized.
Shouldn't an async state machine have a |
This comment has been minimized.
This comment has been minimized.
|
I guess what matters in this context is whether an This is not the same as "drop glue", which can be called through |
This comment has been minimized.
This comment has been minimized.
|
Also, while a generator (and therefore an async state machine) has custom drop glue, it's just to drop the correct set of fields based on the current state, it promises to never move any of the fields during drop. |
This comment has been minimized.
This comment has been minimized.
|
The terminology I use (though I don't think there's any standard): "drop glue" is the compiler generated recursive walking of fields, calling their destructors; "Drop implementation" is an implementation of the |
This comment has been minimized.
This comment has been minimized.
|
Is someone on the hook to write a nomicon chapter on futures? It seems incredibly necessary given how subtle this is. In particular I think examples, especially examples of buggy/bad implementations and how they're unsound, would be illuminating. |
This comment has been minimized.
This comment has been minimized.
|
@Gankro Sure, you can put me down for that. |
Nemo157
referenced this issue
Nov 8, 2018
Merged
Use pinning for generators to make trait safe #55704
This comment has been minimized.
This comment has been minimized.
|
Thanks for the explanation everyone! I personally am super new to async Rust and the Pin APIs, but I have played a bit around with it during the last days (rust-lang-nursery/futures-rs#1315 - where I tried to exploit pinning for intrusive collections in async code). During experimentation I got some concerns with these APIs:
While getting things to compile, I often thought about C++, where most of these issues are avoided: Types can be declared unmovable by deleting the move constructor and assignment operator. When a type is not moveable, types which contain that type are also not. Thereby the property and requirements flow natively through the type hierarchy and get checked by the compiler, instead of forwarding the property inside some calls (not all, e.g. not Regarding Alex What I haven't yet fully understood is, why getting Another thought that I had is whether it's possible to get a simpler system if some limitations are applied. E.g. if things that require pinning could only be used inside async methods and not with future combinators. Maybe that could simplify things to one of On the positive side: Being able to write async/await code with "stack" borrows is pretty cool and much needed! And ability to use pinning for other use-cases, like intrusive collections, will help performance as well as targets like embedded systems or kernels. So I'm really looking forward to a solution on this. |
This comment has been minimized.
This comment has been minimized.
|
Minor nit w.r.t. the stabilization report:
I'm guessing this is actually an |
This comment has been minimized.
This comment has been minimized.
|
@Matthias247 you can't safely get &mut T from One thing I have trouble explaining to myself is what makes Future fundamentally different from other traits such that Pin needs to be part of its API. I mean, I know intuitively it's because async/await requires pinning, but does that say something specifically about Futures that's different from, say, Iterators? Could poll take &mut self and only implement Future for |
This comment has been minimized.
This comment has been minimized.
This actually makes me wonder whether there's a couple of missing methods on impl<'a, T: ?Sized> Pin<&'a T> {
fn map<U: Unpin, F: FnOnce(&T) -> &U>(self, f: F) -> Pin<&'a U>
}
impl<'a, T: ?Sized> Pin<&'a mut T> {
fn map<U: Unpin, F: FnOnce(&mut T) -> &mut U>(self, f: F) -> Pin<&'a mut U>
}These could be used in the I'm trying to work out why this macro claims to be unsafe. If we are The only situation where I can see it being unsound is implementing a custom To make the previous situation safer there could be a wrapper built on pub struct MustPin<T: Unpin>(T, Pinned);
impl<T: Unpin> MustPin<T> {
pub const fn new(t: T) -> Self { ... }
pub fn get(self: Pin<&Self>) -> *const T { ... }
pub fn get_mut(self: Pin<&mut Self>) -> *mut T { ... }
}This all seems backwards compatible with the current API, it could remove some of the unsafety from |
This comment has been minimized.
This comment has been minimized.
|
@Nemo157 Those map functions are unsafe because I could EDIT: Also the pin-utils macro is different, |
This comment has been minimized.
This comment has been minimized.
There's no theoretical difference, but there are pragmatic ones. For one, Another important pragmatic difference is that the code patterns between |
This comment has been minimized.
This comment has been minimized.
Ah, damn, forgot about that part of it |
This comment has been minimized.
This comment has been minimized.
Should these be called
Drop guaranteeI think we have to codify the
"Invalidate" here can mean "deallocation", but also "repurposing": When changing This has the consequence, for example, that it becomes illegal to deallocate a Repurposing
|
This comment has been minimized.
This comment has been minimized.
This is because, so far, all iterators are defined using a type and an However, even if this is not the primary motivation for including generators in the language, it would be very nice to eventually be able to use generator |
This comment has been minimized.
This comment has been minimized.
|
This comment has been minimized.
This comment has been minimized.
|
Ok thanks for the explanations all! I agree with @Gankro that a nomicon-style chapter about In an effort to help myself understand this though I wanted to try again to write down why each function is safe or why it's
|
This comment has been minimized.
This comment has been minimized.
|
@qnighy Its possible for a type to violate pinning guarantees in its Fundamentally, pin projections cannot be made safe without the ability to assert certain negative bounds that can't be expressed in Rust today.
Detach seems a lot better than names like Move and Relocate, but it seems to conflict with other possible uses of the detach metaphor (similar to how Escape could conflict with "escape analysis" etc). There's only so many metaphors we have about how data can relate to other data in computer science, which makes me think of another new advantage of Unpin: by cleaving tightly to the "pin" metaphor, it doesn't occupy space for future metaphoric language we may need to use for a different purpose, the way names like Detach or Escape or Relocate could. |
This comment has been minimized.
This comment has been minimized.
|
@withoutboats I have no particular preference for any name or much investment in this bikeshed... but I'm curious; what other purpose would |
This comment has been minimized.
This comment has been minimized.
|
@Centril I don't have a clear use case in mind, but I could imagine some use case related to destructuring for example. |
This comment has been minimized.
This comment has been minimized.
|
@withoutboats Yeah that makes sense; cheers! |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Dec 10, 2018
•
|
@withoutboats I'm not sure if the Wiktionary entry is the best motivation to justify the naming of In the proposed API, none of the methods has such an effect (and it's not in the scope of pin either). I don't see a safe way for example to transition Edit: More concretely, even if Edit 2: |
This comment has been minimized.
This comment has been minimized.
wmanley
commented
Dec 10, 2018
|
Perhaps |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Dec 10, 2018
|
@wmanley I had a very similar idea, |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Dec 10, 2018
•
|
@withoutboats Follow up question: Since pin seems specified in terms of the underlying memory, how does Pin interact with ZST? Due to |
This comment has been minimized.
This comment has been minimized.
|
Is it safe to create a |
This comment has been minimized.
This comment has been minimized.
|
@HeroicKatora Unfortunately, it's possible today to implement struct S<T>(T); // `!Unpin` if `T: !Unpin`
impl<T> Drop for S<T> { ... } |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Dec 10, 2018
|
@cramertj Thank you, just realized that as well. |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Dec 10, 2018
•
|
@cramertj So we default to |
This comment has been minimized.
This comment has been minimized.
|
@HeroicKatora That would require sprinkling |
This comment has been minimized.
This comment has been minimized.
|
It's also safe to add a PhantomPinned field, as another way of creating a Drop + !Unpin type.
Unpin is just an auto trait like Send and Sync, this proposal involves no new language features. So it has the same semantics as auto traits are already defined to have, which is that they are not applied to generics by default (unlike Sized, which is a built in trait to the compiler, not an auto trait).
We should be clear here that undefined behavior in this context is not really the traditional sort of UB. Rather, its that code, observing a type in a Pin, can make assumptions about the ownership semantics of that value (namely, that if it does not implement Unpin it will not be moved again or invalidated until after its destructor has run). It's not UB in the sense that it is assumed by the compiler never to happen (the compiler doesnt know what a Pin is). So yes, in the sense that you can verify that no code is relying on the guarantee you have provided. But also what you have done is certainly pointless, since no code could observe that the type was pinned. |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Dec 10, 2018
|
@cramertj I see, that would be a bit unfortunate. I'm a bit uncomfortable with
|
This comment has been minimized.
This comment has been minimized.
|
@HeroicKatora the problem with your code is that You rely on the Unpin bound when you call The loophole you thought you found has been considered, which is why there is no way to go from an |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Dec 10, 2018
•
|
@withoutboats Just wanted to get that confirmed, that
since it does not protect any part of the interface, and unwrapping a |
This comment has been minimized.
This comment has been minimized.
|
@HeroicKatora you're correct that that function would be safe. I don't mind adding it but I'd like to avoid expanding the API we're stabilizing in this moment, since this thread is already hundreds of comments long. We can always add it later as the need arises just as we do with all std APIs. |
This comment has been minimized.
This comment has been minimized.
HeroicKatora
commented
Dec 10, 2018
•
|
I would just say that both naming and functionality of Edit: It would mostly just generalize what's already there.
has the instantiation for
|
bors
added a commit
that referenced
this issue
Dec 12, 2018
Mark-Simulacrum
added a commit
to Mark-Simulacrum/rust
that referenced
this issue
Dec 21, 2018
Centril
added a commit
to Centril/rust
that referenced
this issue
Dec 22, 2018
Centril
added a commit
to Centril/rust
that referenced
this issue
Dec 23, 2018
This comment has been minimized.
This comment has been minimized.
|
Now that the stabilization has gone through, the point of the PFCP seems moot, so therefore: @rfcbot cancel |
Centril
closed this
Jan 29, 2019
Centril
reopened this
Jan 29, 2019
This comment has been minimized.
This comment has been minimized.
|
Is there any tracking to make sure the comments developed starting at #55766 (comment) get turned into docs? Seems we are already too late for the release as beta got branched off... even though that was explicitly called out here to happen before stabilization :/ |
withoutboats commentedNov 7, 2018
•
edited
@rfcbot fcp merge
Feature name:
pinStabilization target: 1.32.0
Tracking issue: #49150
Related RFCs: rust-lang/rfcs#2349
This is a proposal to stabilize the
pinlibrary feature, making the "pinning"APIs for manipulating pinned memory usable on stable.
(I've tried to write this proposal as a comprehensive "stabilization report.")
Stabilized feature or APIs
[std|core]::pin::PinThis stabilizes the
Pintype in thepinsubmodule ofstd/core.Pinisa fundamental, transparent wrapper around a generic type
P, which is intendedto be a pointer type (for example,
Pin<&mut T>andPin<Box<T>>are bothvalid, intended constructs). The
Pinwrapper modifies the pointer to "pin"the memory it refers to in place, preventing the user from moving objects out
of that memory.
The usual way to use the
Pintype is to construct a pinned variant of somekind of owning pointer (
Box,Rc, etc). The std library owning pointers allprovide a
pinnedconstructor which returns this. Then, to manipulate thevalue inside, all of these pointers provide a way to degrade toward
Pin<&T>and
Pin<&mut T>. Pinned pointers can deref, giving you back&T, but cannotsafely mutably deref: this is only possible using the unsafe
get_mutfunction.
As a result, anyone mutating data through a pin will be required to uphold the
invariant that they never move out of that data. This allows other code to
safely assume that the data is never moved, allowing it to contain (for
example) self references.
The
Pintype will have these stabilized APIs:impl<P> Pin<P> where P: Deref, P::Target: Unpinfn new(pointer: P) -> Pin<P>impl<P> Pin<P> where P: Derefunsafe fn new_unchecked(pointer: P) -> Pin<P>fn as_ref(&self) -> Pin<&P::Target>impl<P> Pin<P> where P: DerefMutfn as_mut(&mut self) -> Pin<&mut P::Target>fn set(&mut self, P::Target);impl<'a, T: ?Sized> Pin<&'a T>unsafe fn map_unchecked<U, F: FnOnce(&T) -> &U>(self, f: F) -> Pin<&'a U>fn get_ref(self) -> &'a Timpl<'a, T: ?Sized> Pin<&'a mut T>fn into_ref(self) -> Pin<&'a T>unsafe fn get_unchecked_mut(self) -> &'a mut Tunsafe fn map_unchecked_mut<U, F: FnOnce(&mut T) -> &mut U>(self, f: F) -> Pin<&'a mut U>impl<'a, T: ?Sized> Pin<&'a mut T> where T: Unpinfn get_mut(self) -> &'a mut TTrait impls
Most of the trait impls on
Pinare fairly rote, these two are important toits operation:
impl<P: Deref> Deref for Pin<P> { type Target = P::Target }impl<P: DerefMut> DerefMut for Pin<P> where P::Target: Unpin { }std::marker::UnpinUnpin is a safe auto trait which opts out of the guarantees of pinning. If the
target of a pinned pointer implements
Unpin, it is safe to mutablydereference to it.
Unpintypes do not have any guarantees that they will notbe moved out of a
Pin.This makes it as ergonomic to deal with a pinned reference to something that
does not contain self-references as it would be to deal with a non-pinned
reference. The guarantees of
Pinonly matter for special case types likeself-referential structures: those types do not implement
Unpin, so they havethe restrictions of the
Pintype.Notable implementations of
Unpinin std:impl<'a, T: ?Sized> Unpin for &'a Timpl<'a, T: ?Sized> Unpin for &'a mut Timpl<T: ?Sized> Unpin for Box<T>impl<T: ?Sized> Unpin for Rc<T>impl<T: ?Sized> Unpin for Arc<T>These codify the notion that pinnedness is not transitive across pointers. That
is, a
Pin<&T>only pins the actual memory block represented byTin aplace. Users have occassionally been confused by this and expected that a type
like
Pin<&mut Box<T>>pins the data ofTin place, but it only pins thememory the pinned reference actually refers to: in this case, the
Box'srepresentation, which a pointer into the heap.
std::marker::PinnedThe
Pinnedtype is a ZST which does not implementUnpin; it allows you tosupress the auto-implementation of
Unpinon stable, where!Unpinimplswould not be stable yet.
Smart pointer constructors
Constructors are added to the std smart pointers to create pinned references:
Box::pinned(data: T) -> Pin<Box<T>>Rc::pinned(data: T) -> Pin<Rc<T>>Arc::pinned(data: T) -> Pin<Arc<T>>Notes on pinning & safety
Over the last 9 months the pinning APIs have gone through several iterations as
we have investigated their expressive power and also the soundness of their
guarantees. I would now say confidently that the pinning APIs stabilized here
are sound and close enough to the local maxima in ergonomics and
expressiveness; that is, ready for stabilization.
One of the trickier issues of pinning is determining when it is safe to perform
a pin projection: that is, to go from a
Pin<P<Target = Foo>>to aPin<P<Target = Bar>>, whereBaris a field ofFoo. Fortunately, we havebeen able to codify a set of rules which can help users determine if such a
projection is safe:
(Foo: Unpin) implies (Bar: Unpin): thatis, if it is never the case that
Foo(the containing type) isUnpinwhileBar(the projected type) is notUnpin.Baris never moved during the destruction ofFoo,meaning that either
Foohas no destructor, or the destructor is carefullychecked to make sure that it never moves out of the field being projected to.
Foo(the containing type) is notrepr(packed),because this causes fields to be moved around to realign them.
Additionally, the std APIs provide no safe way to pin objects to the stack.
This is because there is no way to implement that safely using a function API.
However, users can unsafely pin things to the stack by guaranteeing that they
never move the object again after creating the pinned reference.
The
pin-utilscrate on crates.io contains macros to assist with both stackpinning and pin projection. The stack pinning macro safely pins objects to the
stack using a trick involving shadowing, whereas a macro for projection exists
which is unsafe, but avoids you having to write the projection boilerplate in
which you could possibly introduce other incorrect unsafe code.
Implementation changes prior to stabilization
Unpinfrom the prelude, removepin::Unpinre-exportAs a general rule, we don't re-export things from multiple places in std unless
one is a supermodule of the real definition (e.g. shortening
std::collections::hash_map::HashMaptostd::collections::HashMap). For thisreason, the re-export of
std::marker::Unpinfromstd::pin::Unpinis out ofplace.
At the same time, other important marker traits like Send and Sync are included
in the prelude. So instead of re-exporting
Unpinfrom thepinmodule, byputting in the prelude we make it unnecessary to import
std::marker::Unpin,the same reason it was put into
pin.Currently, a lot of the associated function of
Pindo not use method syntax.In theory, this is to avoid conflicting with derefable inner methods. However,
this rule has not been applied consistently, and in our experience has mostly
just made things more inconvenient. Pinned pointers only implement immutable
deref, not mutable deref or deref by value, limiting the ability to deref
anyway. Moreover, many of these names are fairly unique (e.g.
map_unchecked)and unlikely to conflict.
Instead, we prefer to give the
Pinmethods their due precedence; users whoneed to access an interior method always can using UFCS, just as they would be
required to to access the Pin methods if we did not use method syntax.
get_mut_uncheckedtoget_unchecked_mutThe current ordering is inconsistent with other uses in the standard library.
impl<P> Unpin for Pin<P>This impl is not justified by our standard justification for unpin impls: there is no pointer direction between
Pin<P>andP. Its usefulness is covered by the impls for pointers themselves.This futures impl will need to change to add a
P: Unpinbound.Pinasrepr(transparent)Pin should be a transparent wrapper around the pointer inside of it, with the same representation.
Connected features and larger milestones
The pin APIs are important to safely manipulating sections of memory which can
be guaranteed not to be moved out. If the objects in that memory do not
implement
Unpin, their address will never change. This is necessary forcreating self-referential generators and asynchronous functions. As a result,
the
Pintype appears in the standard libraryfutureAPIs and will soonappear in the APIs for generators as well (#55704).
Stabilizing the
Pintype and its APIs is a necessary precursor to stabilizingthe
FutureAPIs, which is itself a necessary precursor to stabilizing theasync/awaitsyntax and moving the entirefutures 0.3async IO ecosystemonto stable Rust.
cc @cramertj @RalfJung