Loading page…
Rust walkthroughs
Loading page…
MaybeUninit is a wrapper type for safely handling uninitialized memory in Rust. It replaces the deprecated mem::uninitialized() and mem::zeroed() functions, providing a type-safe way to work with memory that may or may not be initialized.
Key concepts:
T, compiles to raw memoryWhy MaybeUninit is necessary:
mem::uninitialized() was unsound and deprecatedSafety invariants:
MaybeUninit until fully initializedassume_init() only after initialization is completeassume_init_slice() or slice_assume_init_mut()use std::mem::MaybeUninit;
fn main() {
// Create uninitialized memory
let mut uninit: MaybeUninit<i32> = MaybeUninit::uninit();
// Initialize it
uninit.write(42);
// Now safe to assume it's initialized
let value: i32 = unsafe { uninit.assume_init() };
println!("Value: {}", value);
}use std::mem::MaybeUninit;
fn main() {
// Create an uninitialized array
let mut arr: [MaybeUninit<i32>; 5] = MaybeUninit::uninit_array();
// Initialize each element
for (i, elem) in arr.iter_mut().enumerate() {
elem.write((i + 1) as i32);
}
// Convert to initialized array
let arr: [i32; 5] = unsafe {
MaybeUninit::array_assume_init(arr)
};
println!("Array: {:?}", arr);
}use std::mem::MaybeUninit;
struct Point {
x: f64,
y: f64,
}
fn main() {
let mut point = MaybeUninit::<Point>::uninit();
// Initialize field by field using raw pointers
unsafe {
let ptr = point.as_mut_ptr();
// Initialize each field
(*ptr).x = 3.14;
(*ptr).y = 2.71;
}
// Now fully initialized, safe to assume_init
let point: Point = unsafe { point.assume_init() };
println!("Point: ({}, {})", point.x, point.y);
}use std::mem::MaybeUninit;
// Simulate a C function that writes to an out parameter
fn compute_value(output: &mut MaybeUninit<i32>) {
output.write(42);
}
fn main() {
let mut result = MaybeUninit::uninit();
// Pass to function that initializes it
compute_value(&mut result);
// Safe because compute_value initialized it
let value = unsafe { result.assume_init() };
println!("Result: {}", value);
}use std::mem::MaybeUninit;
use std::ffi::c_int;
// C function signature: int get_value(int* out);
// Returns 0 on success, non-zero on error
fn get_value_ffi(output: &mut MaybeUninit<c_int>) -> Result<(), std::io::Error> {
// Simulate calling C function
// In real code: unsafe { get_value(output.as_mut_ptr()) }
// Simulate success
output.write(100);
Ok(())
}
fn main() {
let mut value = MaybeUninit::<c_int>::uninit();
match get_value_ffi(&mut value) {
Ok(()) => {
let v = unsafe { value.assume_init() };
println!("Got value: {}", v);
}
Err(e) => {
println!("Error: {}", e);
}
}
}use std::mem::MaybeUninit;
fn main() {
// Large arrays are slow to initialize with [0; 1000000]
// But sometimes we need to fill each element anyway
const SIZE: usize = 100;
let mut arr: [MaybeUninit<String>; SIZE] = MaybeUninit::uninit_array();
// Initialize each element
for (i, elem) in arr.iter_mut().enumerate() {
elem.write(format!("Item {}", i));
}
// Convert to initialized array
let arr: [String; SIZE] = unsafe {
MaybeUninit::array_assume_init(arr)
};
println!("First: {}, Last: {}", arr[0], arr[SIZE - 1]);
}use std::mem::MaybeUninit;
fn main() {
// Create an uninitialized buffer for reading
let mut buffer: [MaybeUninit<u8>; 1024] = MaybeUninit::uninit_array();
// Simulate reading data (only partially fills buffer)
let bytes_read: usize = 100; // Simulated
// In real code, you'd pass buffer.as_mut_ptr() to a read function
for i in 0..bytes_read {
buffer[i].write(b'X');
}
// Only the first `bytes_read` bytes are initialized
let initialized: &[u8] = unsafe {
MaybeUninit::slice_assume_init(&buffer[..bytes_read])
};
println!("Read {} bytes", initialized.len());
}use std::mem::MaybeUninit;
fn main() {
// Sometimes you need zero-initialized memory
// Safe for types where all-zeros is a valid representation
let mut zeroed: MaybeUninit<i32> = MaybeUninit::zeroed();
let value = unsafe { zeroed.assume_init() };
println!("Zeroed i32: {}", value); // 0
// For types with references, zeroed is NOT safe!
// let bad: MaybeUninit<&i32> = MaybeUninit::zeroed();
// unsafe { bad.assume_init() } // UB! Null reference!
}use std::mem::MaybeUninit;
fn initialize_buffer<T>(size: usize, init_fn: impl Fn(usize) -> T) -> Vec<T> {
let mut buffer: Vec<MaybeUninit<T>> = Vec::with_capacity(size);
// Set length without initializing (unsafe!)
unsafe {
buffer.set_len(size);
}
// Initialize each element
for (i, elem) in buffer.iter_mut().enumerate() {
elem.write(init_fn(i));
}
// Convert to Vec<T>
unsafe {
let ptr = buffer.as_mut_ptr() as *mut T;
let len = buffer.len();
let cap = buffer.capacity();
std::mem::forget(buffer);
Vec::from_raw_parts(ptr, len, cap)
}
}
fn main() {
let numbers = initialize_buffer(10, |i| (i * 2) as i32);
println!("Numbers: {:?}", numbers);
}use std::mem::MaybeUninit;
struct MyVec<T> {
data: Vec<MaybeUninit<T>>,
len: usize,
}
impl<T> MyVec<T> {
fn with_capacity(capacity: usize) -> Self {
let mut data = Vec::with_capacity(capacity);
unsafe {
data.set_len(capacity);
}
Self { data, len: 0 }
}
fn push(&mut self, value: T) {
if self.len < self.data.len() {
self.data[self.len].write(value);
self.len += 1;
} else {
// Would need to reallocate
panic!("Capacity exceeded");
}
}
fn as_slice(&self) -> &[T] {
unsafe {
MaybeUninit::slice_assume_init_ref(&self.data[..self.len])
}
}
}
impl<T> Drop for MyVec<T> {
fn drop(&mut self) {
// Only drop initialized elements
for elem in &mut self.data[..self.len] {
unsafe {
elem.assume_init_drop();
}
}
}
}
fn main() {
let mut vec = MyVec::<String>::with_capacity(10);
vec.push(String::from("Hello"));
vec.push(String::from("World"));
println!("Slice: {:?}", vec.as_slice());
}use std::mem::MaybeUninit;
fn main() {
let mut uninit = MaybeUninit::<i32>::uninit();
// Get raw pointer for writing
let ptr = uninit.as_mut_ptr();
unsafe {
// Write through the pointer
ptr.write(123);
// Read back (safe because we just wrote)
let value = ptr.read();
println!("Value: {}", value);
}
// Or use the safe write method
let mut uninit2 = MaybeUninit::<String>::uninit();
uninit2.write(String::from("Hello"));
let s = unsafe { uninit2.assume_init() };
println!("String: {}", s);
}use std::mem::MaybeUninit;
fn main() {
let source = [1, 2, 3, 4, 5];
let mut dest: [MaybeUninit<i32>; 5] = MaybeUninit::uninit_array();
// Copy from a slice
MaybeUninit::copy_from_slice(&mut dest, &source);
// dest is now fully initialized
let initialized: [i32; 5] = unsafe {
MaybeUninit::array_assume_init(dest)
};
println!("Copied: {:?}", initialized);
}use std::mem::MaybeUninit;
fn main() {
let condition = true;
let mut value = MaybeUninit::<String>::uninit();
let mut initialized = false;
if condition {
value.write(String::from("Initialized!"));
initialized = true;
}
// Only assume_init if we actually initialized
if initialized {
let s = unsafe { value.assume_init() };
println!("Got: {}", s);
} else {
println!("Not initialized");
// Don't call assume_init!
}
}use std::mem::MaybeUninit;
fn main() {
// PITFALL 1: Reading before initialization
let uninit = MaybeUninit::<i32>::uninit();
// unsafe { uninit.assume_init() } // UB! Not initialized!
// PITFALL 2: Partial initialization of structs with Drop
struct HasDrop { x: i32 }
impl Drop for HasDrop {
fn drop(&mut self) { println!("Dropped"); }
}
// If you only partially initialize, drop won't work correctly
// PITFALL 3: Using zeroed for types with niches
// let bad: MaybeUninit<&i32> = MaybeUninit::zeroed();
// unsafe { bad.assume_init() } // UB! Null reference!
// CORRECT USAGE:
let mut good = MaybeUninit::<i32>::uninit();
good.write(42);
let value = unsafe { good.assume_init() };
println!("Correctly initialized: {}", value);
}use std::mem::MaybeUninit;
fn main() {
let mut uninit = MaybeUninit::<String>::uninit();
// Write a value
uninit.write(String::from("Hello"));
// Drop the value without taking it
unsafe {
uninit.assume_init_drop();
}
// Now uninit is back to uninitialized state
// Can safely be dropped without double-free
println!("Value dropped properly");
}use std::mem::MaybeUninit;
fn main() {
// MaybeUninit has the same size and layout as T
// It's safe to transmute between them (but usually unnecessary)
let value: i32 = 42;
// These are equivalent:
let uninit1: MaybeUninit<i32> = MaybeUninit::new(value);
// transmute (advanced use only)
// let uninit2: MaybeUninit<i32> = unsafe {
// std::mem::transmute(value)
// };
println!("Size of MaybeUninit<i32>: {}", std::mem::size_of::<MaybeUninit<i32>>());
println!("Size of i32: {}", std::mem::size_of::<i32>());
// Same size!
}MaybeUninit Methods:
| Method | Description | Safety |
|--------|-------------|--------|
| uninit() | Create uninitialized memory | Safe |
| zeroed() | Create zero-initialized memory | Safe (but zero may be invalid) |
| new(value) | Create from initialized value | Safe |
| write(value) | Initialize with value | Safe |
| assume_init() | Extract the value | Unsafe |
| assume_init_drop() | Drop without extracting | Unsafe |
| as_ptr() | Get *const T | Safe |
| as_mut_ptr() | Get *mut T | Safe |
| uninit_array() | Create uninitialized array | Safe (const fn) |
| array_assume_init() | Convert array to initialized | Unsafe |
| slice_assume_init_ref() | Convert slice to reference | Unsafe |
| slice_assume_init_mut() | Convert mut slice to mut ref | Unsafe |
Safety Checklist:
| Action | Safe? |
|--------|-------|
| Creating MaybeUninit::uninit() | ✅ Safe |
| Writing with .write(value) | ✅ Safe |
| Reading with .assume_init() | ⚠️ Unsafe, requires initialization |
| Dropping with .assume_init_drop() | ⚠️ Unsafe, requires initialization |
| Using zeroed() for primitive types | ✅ Safe |
| Using zeroed() for reference types | ❌ UB (null reference) |
When to Use MaybeUninit:
| Scenario | Appropriate? | |----------|-------------| | FFI with out parameters | ✅ Yes | | Large array initialization | ✅ Yes | | Custom collections | ✅ Yes | | Performance-critical buffers | ✅ Yes | | Normal variable initialization | ❌ Use normal binding | | Simple struct creation | ❌ Use normal initialization |
Comparison with Related Types:
| Type | Initialization | Drop | Use Case |
|------|----------------|------|----------|
| T | Required | Automatic | Normal values |
| MaybeUninit<T> | Manual | Manual | Uninitialized memory |
| ManuallyDrop<T> | Required | Manual | Prevent auto-drop |
| UnsafeCell<T> | Required | Automatic | Interior mutability |
Key Points:
MaybeUninit<T> is for safely handling uninitialized memoryMaybeUninit until fully initializedassume_init() is unsafe and requires initialization to be completezeroed() only for types where all-zeros is validT but no drop until you explicitly call itManuallyDrop for complex memory management