Unchecked Uninitialized Memory
One interesting exception to this rule is working with arrays. Safe Rust doesn’tpermit you to partially initialize an array. When you initialize an array, youcan either set every value to the same thing with let x = [val; N]
, or you canspecify each member individually with let x = [val1, val2, val3]
.Unfortunately this is pretty rigid, especially if you need to initialize yourarray in a more incremental or dynamic way.
Unsafe Rust gives us a powerful tool to handle this problem:mem::uninitialized
. This function pretends to return a valuewhen really it does nothing at all. Using it, we can convince Rust that we haveinitialized a variable, allowing us to do trickier things with conditional andincremental initialization.
Unfortunately, this opens us up to all kinds of problems. Assignment has adifferent meaning to Rust based on whether it believes that a variable isinitialized or not. If it’s believed uninitialized, then Rust will semanticallyjust memcopy the bits over the uninitialized ones, and do nothing else. Howeverif Rust believes a value to be initialized, it will try to Drop
the old value!Since we’ve tricked Rust into believing that the value is initialized, we can nolonger safely use normal assignment.
This is also a problem if you’re working with a raw system allocator, whichreturns a pointer to uninitialized memory.
To handle this, we must use the ptr
module. In particular, it providesthree functions that allow us to assign bytes to a location in memory withoutdropping the old value: write
, copy
, and copy_nonoverlapping
.
ptr::write(ptr, val)
takes aval
and moves it into the address pointedto byptr
.ptr::copy(src, dest, count)
copies the bits thatcount
T’s would occupyfrom src to dest. (this is equivalent to memmove — note that the argumentorder is reversed!)ptr::copy_nonoverlapping(src, dest, count)
does whatcopy
does, but alittle faster on the assumption that the two ranges of memory don’t overlap.(this is equivalent to memcpy — note that the argument order is reversed!)
It should go without saying that these functions, if misused, will cause serioushavoc or just straight up Undefined Behavior. The only things that thesefunctions themselves require is that the locations you want to read and writeare allocated. However the ways writing arbitrary bits to arbitrarylocations of memory can break things are basically uncountable!
Putting this all together, we get the following:
use std::mem;
use std::ptr;
// size of the array is hard-coded but easy to change. This means we can't
// use [a, b, c] syntax to initialize the array, though!
const SIZE: usize = 10;
let mut x: [Box<u32>; SIZE];
unsafe {
// convince Rust that x is Totally Initialized
x = mem::uninitialized();
for i in 0..SIZE {
// very carefully overwrite each index without reading it
// NOTE: exception safety is not a concern; Box can't panic
ptr::write(&mut x[i], Box::new(i as u32));
}
}
println!("{:?}", x);
It’s worth noting that you don’t need to worry about ptr::write
-styleshenanigans with types which don’t implement Drop
or contain Drop
types,because Rust knows not to try to drop them. Similarly you should be able toassign to fields of partially initialized structs directly if those fields don’tcontain any Drop
types.
However when working with uninitialized memory you need to be ever-vigilant forRust trying to drop values you make like this before they’re fully initialized.Every control path through that variable’s scope must initialize the valuebefore it ends, if it has a destructor.This includes code panicking.
Not being careful about uninitialized memory often leads to bugs and it has beendecided the mem::uninitialized
function should be deprecated.The MaybeUninit
type is supposed to replace it as its API wraps many commonoperations needed to be done around initialized memory. This is nightly only fornow.
And that’s about it for working with uninitialized memory! Basically nothinganywhere expects to be handed uninitialized memory, so if you’re going to passit around at all, be sure to be really careful.