How do I work with MaybeUninit in Rust?

Walkthrough

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:

  • Uninitialized memory — Memory with arbitrary, undefined contents
  • Safe wrapper — Prevents undefined behavior from reading uninitialized data
  • Zero-cost — Same size as T, compiles to raw memory
  • Gradual initialization — Build values field by field

Why MaybeUninit is necessary:

  • Reading uninitialized memory is undefined behavior
  • mem::uninitialized() was unsound and deprecated
  • Some APIs require out parameters
  • Performance-critical code may need to skip initialization

Safety invariants:

  • Never read from MaybeUninit until fully initialized
  • Use assume_init() only after initialization is complete
  • For arrays, use assume_init_slice() or slice_assume_init_mut()

Code Examples

Basic MaybeUninit Usage

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);
}

Uninitialized Array

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);
}

Gradual Initialization

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);
}

Out Parameter Pattern

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);
}

FFI with Out Parameters

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);
        }
    }
}

Initializing Large Arrays

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]);
}

Uninitialized Buffer

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());
}

Zeroed Memory

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!
}

MaybeUninit with Generic Types

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);
}

Building a Vec-style Structure

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());
}

Read and Write with Raw Pointers

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);
}

Copy from Slice

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);
}

Conditional Initialization

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!
    }
}

Common Pitfalls

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);
}

Dropping Uninitialized Memory

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");
}

Transmute with MaybeUninit

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!
}

Summary

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 memory
  • Never read from MaybeUninit until fully initialized
  • assume_init() is unsafe and requires initialization to be complete
  • Use zeroed() only for types where all-zeros is valid
  • Essential for FFI, large arrays, and custom collections
  • Same size as T but no drop until you explicitly call it
  • Combines well with ManuallyDrop for complex memory management