Invariants on the address of a variable guaranteed by the compiler

I need to create a structure whose memory address must be kept constant (the address is sent to a CPU register which refers to it afterwards). I read from here Variable locations in Rust during copy and move | Kyle M. Douglass for example that values can change locations, I guess that Vecs can change too when updating capacity (reallocating memory somewhere else if they need more space than available where they where first allocated) and I read somewhere else that even Boxes can change location (the inner value, not the pointer itself).
Is wrapping the structure with a Pin sufficient to ensure that the structure does not change its address afterwards, even when I move the value around? Is it even necessary in my case? I fail to see in which cases the compiler can change the location of a value, and how Pin enforces the fixed location of my structure.
This begs a second question: when does the compiler change the location of a value, and how does Pin enforces the fixed location?
From what I understand, Pin's magic for !Unpin values relies on two facts: it wraps a pointer to my value, and I can never have access to this value anymore without some unsafe code (so it's safe because it's locked, or sort of). And if I nevertheless want to access it (get_unchecked_mut) I have to guarantee I will not move it. But how then can I be sure I don't move it when using it with get_unchecked_mut? (relates to the second question: when does the compiler change the location of a value, except for explicit cases like std::swap)

If you own a Box<T>, and do not move the T out, the T will not be moved; its address is stable.

Pin<Box<T>> is only necessary when the T in the box needs a safety guarantee that it won't be moved that doesn't rely on its owner cooperating.

3 Likes

Does it mean that if I don't explicitly move T outside of my box, but do things on it (call some of its methods), the compiler will not move its location? (For ex. some kind of !Sized structure which would require implicit relocation when doing stuff on it)

Depends on the method:

  • If you call a method that takes self, that would (always) move the value. (And your Box<T> is then invalidated.)
  • If you call a method that takes &self, the value cannot be moved or mutated.
  • If you call a method that takes &mut self, it could mutate or replace the self value — there will still be a valid value at that address but not the original one. (Pin<&mut Self> would be similar to this but allows fewer kinds of mutation, unless T: Unpin.)

The same applies to any function that takes T, &T, or &mut T.

1 Like

Rust will not move a value if you don't actually insert any explicit move operations.

2 Likes

When does a move occur? I think it is every time I do sthg like that:

let a = /* */;
let b= a;

Or when I return a value from a function, or pass a value to a function (e.g. fn foo(t: T)), which would include take() for example
And does that mean that whenever there's a move, there's a possibility that the value changes its address?

Yeah, moves happen when you make those kinds of operations. And yes, the address may change on a move. (It also may not, e.g. due to optimizations.)

1 Like

Perhaps you're referring to the Boxed case or heap data specifically, but that's not guaranteed in the general case.

No, I really do mean stack locations too. Many uses of Pin are incorrect if that is not guaranteed. For example, if the destructor of a variable is not guaranteed to be given the same address as the mutable reference last created to it, then the futures::pin_mut! and tokio::pin! macros are not correct, and the same would apply to the unstable pin! macro in the standard library.

1 Like

And when you've got a mutable reference to a value, is the only pitfall std::swaping (or replacing manually)? Or is there other things to be aware of? In the context of a reference to a pinned value, for example, that we retrieve with get_unchecked_mut.