4 ms·
Trivially moveable means that you can move the object by simply copying the bytes to a new location, and non-trivially-moveable objects are those that would be
by aliceryhl 5y ago
Trivially moveable means that you can move the object by simply copying the bytes to a new location, and non-trivially-moveable objects are those that would be incorrect to move in this manner. A move constructor is a C++ concept for non-trivially-moveable types that lets you do something more complicated than just copying the bytes over when you want to move the value to a new location, and creating a move constructor is essentially how you overload the assignment operator in C++.
The classic example of something that is not trivially moveable is something with a pointer into itself. If you just copy the bytes, that pointer still points at the old location, which would not be valid. In C++, you would handle that by having a move constructor update the pointer to point at the new location.
Dealing with non-trivially-moveable types make memory safe APIs tricky because Rust generally guarantees that code with no unsafe block can never result in memory unsafety. This also applies to library design, and anyone designing a library with unsafe blocks in it should design their library to provide the same guarantee — i.e. that anyone using only the safe methods from the library may not be able to cause memory unsafety, even if they can call methods in the library containing unsafe blocks. This should hold no matter how ridiculous the thing they're doing is, as long as it doesn't require unsafe.
So the reason why this is tricky is that safe Rust permits anyone to perform a "trivial move" of any object they have ownership of. Thus, if you make even the constructor of such an object safe, then safe code could call that constructor, then move the object in an unsound way.
So how do you solve this? Any library that has a non-trivially-moveable type needs some way for the user to promise not to move it using a trivial move. The way they do this is by introducing a Pin type, which is a wrapper around a (typically mutable) reference. The primary constructor for Pin is called new_unchecked, and it is an unsafe method whose safety requirement for calling it is that you never move the object that the reference points at. Thus, any library that sees a reference wrapped in a Pin knows that whoever created it _must_ have called an unsafe block, promising to follow the rules for that unsafe method. It is, in a sense, a way to shift the blame if anything goes wrong in an unsafe block in the library — normally the library would be at blame because the library is the only one with an unsafe block, but when using Pin, it is the person who _created_ the Pin object who is at blame for the incorrect use of unsafe — not the library.
It is worth noting that new_unchecked is not the only way to create a Pin. For example, there's a macro called pin! that uses variable shadowing to prevent the user from accessing the underlying variable, preventing the user from moving the object since they can't access the variable name anymore. This lets safe code create a Pin reference to something the safe code owns, and the unsafe block is inside the definition of the macro.
Regarding moving references outside unsafe code, well that's because if someone gets hold of a mutable reference to a pinned value, then they could call std::mem::swap to perform a trivial move of that value, and this would not involve an unsafe block. Thus, if the library exposes any way of getting such a mutable reference in safe code, then that would be an incorrect unsafe block, as safe code is allowed to do anything it wants, as long as it uses only unsafe.
The Pin wrapper type has methods for unwrapping it and getting back the mutable reference that it is a wrapper around, however they are unsafe, so if anyone did that and then called std::mem::swap, they would be at fault because the one who unwrapped it called an unsafe method without following its rules.
As for Unpin, that's a marker on types that are trivially moveable. The Pin wrapper is essentially a no-op for references to those types and provides no guarantees. There's a safe Pin::new method that can only be used for such types.
It's worth pointing out that in Rust, non-trivially-moveable types must still be trivially moveable _until_ they get pinned. So they typically have an "init" state or something like that which can be safely moved around, but then once you pin it, the object becomes non-trivially-moveable.
- Tamschi 5y agoSomething else that I didn't mention in the post is that `Pin<Box<T>>` is `From<Box<T>>` (so it can be cast directly, and here without reallocation or such), and this could also be implemented for (other) exclusively-owning containers with heap storage. The standard library collections don't do this because they don't have an API that would make it useful. It suspect it's partly because you can't `self: &mut Pin<Self>` in stable, which would be needed to cleanly give them one, but there's also a fair argument for not making them kitchen-sink types while a wrapper could provide this easily.
- maxbrunsfeld 5y agoThis is an amazing summary. Thank you.
- sfvisser 5y agoNearly all my confusion is gone. This is amazing, thanks!