So, ptr::addr_of{,_mut}!
is a macro used to express &raw {,mut}
expressions, which are able to produce a pointer to a valid place without going through / without producing an intermediary Rust reference, since a Rust reference:
-
not only does it require to refer to a valid place,
-
but it may also require to refer to a valid expression,
-
and it does assert the aliasing or lack thereof guarantees that shared and exclusive references comes with:
-
Shared reference case: &x as *const _
Unless T
contains shared mutability (UnsafeCell
) somewhere inside it, a shared reference to a T
asserts the immutability of the referee, T
, from the point in time where it is produced, until the point in time where that reference and any pointer derived from it is last used.
It also invalidates the &mut
aspect of any other outstanding pointers —except for the pointer/ref, if any, from which the shared reference has been derived, since that counts as a reborrow, and thus, after the aforementioned point of last use, such pointer gets its &mut
capabilities back.
-
Exclusive reference case: &mut x as *mut _
An exclusive reference to a T
asserts the lack of aliasing to that T
, thus fully invalidating any other outstanding pointer or reference to that T
—except for the pointer/ref, if any, from which the exclusive reference has been derived, since reborrow yadda yadda…
So usage of ptr::addr_of{,mut}!
avoids asserting the invariants from 2.
and 3.
, which makes their usage less prone to Undefined Behavior, which is a big win indeed. It does, however, require to be fed a "valid place" (so as to, for instance, allow some compile backends to perform optimized pointer arithmetic). Note that the wording "valid place" is one I've just come up with, and may not be the most accurate one.
A) The challenges of a "just valid place" / avoiding the requirements 2.
and 3.
by using MaybeUninit
and ptr::addr_of{,mut}
Note that I'm unsure about the rules regarding non-null and yet dangling pointers, or unaligned pointers. I won't talk about the latter given this lack of knowledge, and for the remainder of the post, I'll make the conservative assumption, regarding the former, that dangling pointers, not even non-null, do not point to valid places (which seems to contradict the intention behind @quinedot's quote of the RFC, but since, IIRC, ptr::addr_of…!
is allowed to unsugar to LLVM's GEPi, and since that operation is UB with a dangling pointer, I remain skeptical regarding that expectation in the RFC. Take the ::memoffset
crate, for instance, the most official / canonical implementation of the offsetof!
operation: such crate does not use a NonNull::dangling()
to compute the offsets; instead it does use MaybeUninit
to get hold of a non-dangling pointer to an instance of the desired type.
Skip this if you already know well the field-by-field initialization of an uninit struct pattern, and why it must be written that way (e.g. the OP)
The key rule for a "valid place" is that a place must refer to the region of memory of (part of) an allocated element/instance/entity. Such can be obtained:
-
from a valid (and thus initialized (for its type)) entity: let x = 42; ptr::addr_of!(x);
- Obviously, once the instance's storage is deallocated (e.g., binding goes out of scope), the place it occupied is no longer valid / accessible, and so any pointers pointing to it are then dangling / invalidated.
-
the pointee of a non-null Allocator
-yielded pointer: a "heap" pointer pointing to uninit.
- Until that pointer is
{r,d}ealloc
ated
-
from the backing storage (init or uninit), of 1.
- Same as for
1.
, the place is only accessible until the instance it was derived from is deallocated / goes out of scope.
This is how one can easily obtain a "stack" pointer pointing to uninit: one starts with a valid (and thus initialized (for its type)) value of the MaybeUninit
type: no matter the type T
, MaybeUninit<T>
has a trivial and cheap valid instance of that type, MaybeUninit::uninit()
, which is an instance occupying the same size as an instance of type T
would have occupied, but without caring about the value of any of its backing bytes, hence featuring uninit bytes.
use ::core::mem::{MaybeUninit, MaybeUninit as MU};
let mut value: MU<T> = MaybeUninit::uninit();
We can then take a pointer to that place thanks to 1.
:
let p_uninit: *mut MU<T> = &mut value as _; // asserts lack of aliasing! It's OK.
// or:
let p_uninit: *mut MU<T> = ptr::addr_of_mut!(value); // place not derived from a pointer, so it also asserts lack of aliasing (to get `mut` rights).
And now that we have a raw pointer, we are free to toy with / change the type of the pointee, provided we remain in the "raw pointer realm" (no Rust references!):
let p: *mut T = <*mut MU<T>>::cast::<T>(p_uninit);
- FWIW, this is such a pervasive pattern that
MaybeUninit<T>
offers a convenience method for this: let p: *mut T = value.as_mut_ptr();
And now let's consider the place *p
refers to. *p
is of type T
, so the "validity requirements" for that place are that at least part of some real instance (of whatever type) at least as big as T
have been allocated there already. And we do meet that requirement, thanks to value
, whose address is where p
points to. So *p
is a valid place, and thanks to the mut
able provenance that the pointer is imbued with (whether it originated from &mut value
, or ptr::addr_of_mut!(value)
, or value.as_mut_ptr()
), it's even a place that will, on its own, yield a pointer with mutable provenance / capabilities as well (an example where this wouldn't have hold is if p_uninit
had been constructed using &value
or ptr::addr_of!(value)
).
Thus, ptr::addr_of_mut!(*p)
is yielding a pointer identical to p
: same address (guaranteed!), and same mut
capabilities / provenance.
While doing addr_of_mut!(*p)
is not very interesting on its own (it's literally the "inverse" of that *
operator), things become more interesting, for the "initialize field-by-field" pattern (be it for a pointer to uninit memory in the heap, or in the stack), when T
is, say, a struct
or a tuple. Say T = (A, B);
, and say we want to initialize that very pointee T
since we do have a value of type A
and of type B
:
/// # Safety (invariants / requirements for callers to uphold):
///
/// - `out` must be a pointer with mutable capabilities over its pointee;
///
/// - well-aligned? (if we used `.write_unaligned()` below then I'm not sure about this one being mandatory, but when in doubt, it's better to be conservative and require it)
///
/// - no parallel mutation of `*out` during this call.
unsafe
fn init<A, B> (
out: *mut (A, B),
a: A,
b: B,
)
{
unsafe {
// Safety (requirements that us, the callee, do uphold for this to be OK).
// - From the call-site required invariants, we are allowed to compute
// this `out_a` pointer, and it will have mutable capabilites over
// the whole `*out` range, so, *a fortiori*, over `(*out).0`.
let out_a = ptr::addr_of_mut!( (*out).0 );
// - well-aligned (since `out` was), and no parallel mutation
out_a.write(a);
}
unsafe {
// Symmetrical safety comments.
let out_b = ptr::addr_of_mut!( (*out).1 );
out_b.write(a);
}
}
For those not fully aware of this, in the more general case (arbitrary types A
, and B
), it is impossible to write a field-by-field initialization of a repr(Rust)
struct (the body of init
, here), without access to ptr::addr_of_mut!
.
Granted, many cases do not need field-by-field init, and in this instance, doing out.write((a, b));
would have been a way simpler implementation.
But consider the case where B = &A
, and so where we would like to write out_b.write(at_a);
In that case, to obtain such a at_a
reference without using either of ptr::addr_of{,_mut}!
would have been difficult and error-prone at best, and impossible in the general case (the only way that comes to mind would be to abuse having a distinct "dummy" (but valid!) &A
reference somewhere, so as to do out.write((a, dummy_ref));
so that we'd then be able to use &(*out).0
and &mut (*out).1
without triggering UB).
B) Avoiding the requirements of 3. (aliasing or lack thereof) only
In this instance, since we don't mind asserting validity (of expr) invariants, MaybeUninit
may not be necessary.
But that doesn't mean that we shouldn't be mindful of aliasing (or lack thereof) invariants.
Indeed, let's consider:
struct DoublyLinkedNode {
prev: *mut Self, // `Option<ptr::NonNull<Self>>` is better, but skipped here to keep the example overly simple.
next: *mut Self,
}
impl DoublyLinkedNode {
unsafe // Safety: `node`'s `next` and `prev` pointers,
// if non-null, are well-aligned pointers to other
// `DoublyLinkedNode` instances, with mutable capabilities (SharedReadWrite)
// and with no mutation happening in parallel of this call.
fn unlink (cur_node: &Cell<DoublyLinkedNode>)
{
let prev = cell_project!(DoublyLinkedNode, cur_node.prev).get();
if prev.is_null().not() {
ptr::addr_of_mut![ (*prev).next ]
.write(ptr::null_mut())
;
}
// Same for `next`
}
}
In this snippet, the usage of ptr::addr_of_mut!()
is one way to avoid the lack-of-aliasing-violation-induced unsoundness that we'd have triggered if we had written:
// Unsound!
if prev.is_null().not() {
(*prev).next = ptr::null_mut();
}
Indeed, the .next = …
assignment to a field uses a &mut
under the hood, thus asserting lack of aliasing w.r.t. that next
field of the prev
ious node, hence invalidating any other outstanding pointers to that node!
- Such as
prev_prev.next
(where prev_prev
would be the node at position .prev.prev
w.r.t. cur_node
)). So, for instance, if we were to follow up this unregistering of cur_node
with prev_prev
checking to see how many nodes would be available along its .next
chain, it would try to go and check if (*prev_prev.next).next
is null, thus performing a read across an invalidated pointer, and thus UB.
So there are two ways main ways of fixing that unsoundness:
if prev.is_null().not() {
- (*prev).next = ptr::null_mut();
+ let prev: &Cell<DoublyLinkedNode> = &*prev.cast();
+ cell_project!(DoublyLinkedNode, prev.next).set(ptr::null_mut());
or what I did in the example:
if prev.is_null().not() {
- (*prev).next = ptr::null_mut();
+ ptr::addr_of_mut![ (*prev).next ].write(ptr::null_mut());
- EDIT: fixed typo, thanks @fee1-dead for reporting it!
FWIW, I initially thought that:
was gonna involve this, but it just turns out that they do use uninit memory there as well; they're just using Box::new_uninit()
for that.