Understaning Pin (for C and C++ Developers)

Pin is pretty important for Rust’s recently-released async.await
features. I read the docs. I didn’t get it1. This exercise is what it
took for me to understand why Pin is important.

Opening up the documentation, the page starts with a discussion about
Unpin. Unpin is weird. Basically, Unpin says “yeah I know this
is pinned but you are free to ignore that.” My gut reaction to Unpin
was “why would you need this at all?” Doesn’t this defeat the purpose
of Pin? Why is everything Unpin by default??

Continuing on, there’s a list of rules which must be adhered to in the
unsafe constructor for Pin. I found this constraint for types
which are !Unpin to be particularly mysterious:

It must not be possible to obtain a &mut P::Target and then move out
of that reference (using, for example mem::swap).

Other guides to Pin also noted that calling mem::replace, which
also takes a mutable reference, cannot not be allowed.

Let’s look at this again:

It must not be possible to obtain a &mut P::Target and then move out
of that reference (using, for example mem::swap).

Clearly moving is significant here, what does that mean exactly, and
why is this such a big deal?

C++

I’m more familiar with C++ and my familiarity is probably where my
misunderstandings are coming from. Let’s start by understanding what
it means to move something in C++.

As far as I know, this is totally valid C++. The reference is just a
pointer to some chunk of memory, and, all of the moves that we did are
defined to leave the moved-from object in a “valid” state (you might
just have to be careful with them).

Let’s consider one last struct.

template<typenameT, size_tN>structring_buffer{std::array<T, N+1>entries; // use one extra element for easy book-keeping// Store pointers. This is bad, there are better ways to make a ring// buffer, but the demonstration is useful.T* head = entries;
T* tail = head+1;
// ...};

head and tail both point to elements of entries. C++ will
generate a default move constructor for us, but the default is just a
memcpy. If it runs, we’ll end up with pointers that point into the
wrong array. We must write a custom move constructor.

This prints “1” because the compiler reused the stack space used by
the object named a to store the object named b. There was no
“empty valid husk” left behind.

This behavior is very different from the C++ move. The Rust compiler
knows about the move and can take advantage of the move to save some
stack space. Without writing unsafe code, there is no way you’d ever
be able to access fields from a again, so how the compiler wants to
use that space occupied by a after the move is entirely the
compiler’s decision.

Rule number 1 of Rust move: The compiler knows you moved. The compiler
can use this to optimize.

The next C++ example was a swap. In C++, swap calls some move
constructors to shuffle the data around. In the C++ swap example,
these (implicit) move constructors where just memcpy.

Swap in Rust isn’t as straightforward as the C++ version. In the C++
version, we just call the user defined move constructor to do all of
the hard work. In Rust, we don’t have this user defined function to
call, so we’ll have to actually be explicit about what swap does.
This version of swap is adapted from Rust’s standard library:

This example is nice because it does what you’d expect, but it
highlights something critical about Rust’s move semantics: move is
always a memcpy. move in Rust couldn’t be anything other than a
memcpy. Rust doesn’t define anything else associated with the struct
that would let the user specify any other operation.

Rule number 2: Rust move is always just a memcpy.

Now, let’s think about the ring buffer. It is not even remotely
idiomatic to write anything like the C++ version of the ring-buffer in
Rust3, but let’s do it anyway. I’m also going to pretend that const
generics are finished for the sake of clarity.

The problem now is that we can’t define a custom move constructor. If
this struct is ever moved (including the move-by-memcpy in
swap/replace), the pointers stored will be point to the wrong piece of
memory.

The rust solution to this is to mark your type as !Unpin.

Once something is marked as !Unpin, getting a mutable reference to
it becomes unsafe. If you get a mutable reference to a pinned type
which is !Unpin, you must promise to never call anything that moves
out of the type. I have thoughts on the actual feasibility of
following these rules, but that’s a topic for another time.

Futures/async.await

Hopefully now, we can understand why this is prerequisite for
async.await support in Rust.

The compiler will roughly translate this function into a state machine
with 2 states. That state machine is represented by some struct, and
the state is updated by calling the poll function. The struct used
to store the data for this state machine will look something like
this: