How do I work with ManuallyDrop in Rust?

Walkthrough

ManuallyDrop is a wrapper that prevents Rust from automatically dropping the inner value. It's useful when you need manual control over when and how a value is destroyed, such as when working with raw pointers, FFI, or implementing custom allocation strategies.

Key characteristics:

  • No automatic drop — The inner value is never automatically dropped
  • Zero overhead — Same size and layout as T
  • unsafe required — Dropping requires calling unsafe methods
  • Manual control — You decide when to drop via drop() or take()

When to use ManuallyDrop:

  • Implementing custom smart pointers
  • FFI with ownership transfer
  • Building collection types (Vec, HashMap internals)
  • Lazy initialization patterns
  • Avoiding double-drop in unsafe code

Warning: Forgetting to manually drop a ManuallyDrop value causes memory leaks!

Code Examples

Basic ManuallyDrop Usage

use std::mem::ManuallyDrop;
 
fn main() {
    // Create a ManuallyDrop wrapper
    let mut value = ManuallyDrop::new(String::from("Hello, World!"));
    
    // Access the inner value normally
    println!("Value: {}", *value);
    
    // The String will NOT be dropped when `value` goes out of scope!
    // This would leak memory if we don't drop it manually
    
    // Manually drop the value
    unsafe {
        ManuallyDrop::drop(&mut value);
    }
    
    println!("Value has been manually dropped");
}

Preventing Automatic Drop

use std::mem::ManuallyDrop;
 
struct ImportantResource {
    data: String,
}
 
impl Drop for ImportantResource {
    fn drop(&mut self) {
        println!("Dropping: {}", self.data);
    }
}
 
fn main() {
    {
        // Normal drop
        let normal = ImportantResource { data: String::from("normal") };
        // Drop happens here automatically
    }
    println!("---");
    
    {
        // ManuallyDrop prevents automatic drop
        let manual = ManuallyDrop::new(ImportantResource { data: String::from("manual") });
        // No drop happens!
        println!("Exiting scope...");
    }
    println!("(No drop message above - memory leaked!)");
    
    // Proper usage:
    let mut manual = ManuallyDrop::new(ImportantResource { data: String::from("proper") });
    unsafe {
        ManuallyDrop::drop(&mut manual);
    }
}

Taking the Value Out

use std::mem::ManuallyDrop;
 
fn main() {
    let mut manual = ManuallyDrop::new(vec![1, 2, 3, 4, 5]);
    
    // take() extracts the value without dropping it
    let vec: Vec<i32> = unsafe { ManuallyDrop::take(&mut manual) };
    
    println!("Extracted: {:?}", vec);
    
    // The ManuallyDrop is now empty - no drop will occur
    // The Vec will be dropped normally when `vec` goes out of scope
}

Implementing a Custom Smart Pointer

use std::mem::ManuallyDrop;
use std::ops::Deref;
use std::ptr::NonNull;
 
struct MyBox<T> {
    ptr: NonNull<T>,
    // We need to drop the allocation, but we also need to drop T
    // ManuallyDrop lets us control this
}
 
impl<T> MyBox<T> {
    fn new(value: T) -> Self {
        let boxed = Box::new(value);
        let ptr = NonNull::new(Box::into_raw(boxed)).unwrap();
        Self { ptr }
    }
}
 
impl<T> Deref for MyBox<T> {
    type Target = T;
    
    fn deref(&self) -> &T {
        unsafe { self.ptr.as_ref() }
    }
}
 
impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        unsafe {
            // First drop the value
            std::ptr::drop_in_place(self.ptr.as_ptr());
            // Then deallocate the memory
            let _ = Box::from_raw(self.ptr.as_ptr());
        }
    }
}
 
fn main() {
    let my_box = MyBox::new(String::from("Hello"));
    println!("Value: {}", *my_box);
    // Dropped properly when my_box goes out of scope
}

Implementing a Union

use std::mem::ManuallyDrop;
 
// Unions require ManuallyDrop for non-Copy types
#[derive(Debug)]
enum Value {
    Integer(i64),
    Float(f64),
}
 
union SafeUnion {
    int: i64,
    float: f64,
    // For non-Copy types, you would use:
    // string: ManuallyDrop<String>,
}
 
fn main() {
    let mut u = SafeUnion { int: 42 };
    
    unsafe {
        println!("Integer: {}", u.int);
        
        // Write a float
        u.float = 3.14;
        println!("Float: {}", u.float);
    }
}

Union with Non-Copy Types

use std::mem::ManuallyDrop;
 
union StringOrInt {
    string: ManuallyDrop<String>,
    int: i64,
}
 
impl StringOrInt {
    fn from_string(s: String) -> Self {
        Self {
            string: ManuallyDrop::new(s),
        }
    }
    
    fn from_int(i: i64) -> Self {
        Self { int: i }
    }
    
    fn as_string(&self) -> Option<&String> {
        // In real code, you'd need a tag to know which variant
        // This is simplified for demonstration
        unsafe { Some(&self.string) }
    }
}
 
impl Drop for StringOrInt {
    fn drop(&mut self) {
        // In real code, check a tag to know which variant to drop
        // This example always drops the string variant (incorrect!)
        // This is why unions need careful handling
    }
}
 
fn main() {
    let string_val = StringOrInt::from_string(String::from("hello"));
    unsafe {
        println!("String: {}", *string_val.string);
    }
    // Proper cleanup needed!
}

Building a Vec-like Structure

use std::mem::ManuallyDrop;
 
struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    capacity: usize,
}
 
impl<T> MyVec<T> {
    fn new() -> Self {
        Self {
            ptr: std::ptr::null_mut(),
            len: 0,
            capacity: 0,
        }
    }
    
    fn push(&mut self, value: T) {
        if self.len == self.capacity {
            self.grow();
        }
        
        unsafe {
            std::ptr::write(self.ptr.add(self.len), value);
        }
        self.len += 1;
    }
    
    fn grow(&mut self) {
        let new_capacity = if self.capacity == 0 { 4 } else { self.capacity * 2 };
        let new_layout = std::alloc::Layout::array::<T>(new_capacity).unwrap();
        
        let new_ptr = if self.capacity == 0 {
            unsafe { std::alloc::alloc(new_layout) as *mut T }
        } else {
            let old_layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
            unsafe {
                std::alloc::realloc(self.ptr as *mut u8, old_layout, new_layout.size()) as *mut T
            }
        };
        
        self.ptr = new_ptr;
        self.capacity = new_capacity;
    }
    
    fn len(&self) -> usize {
        self.len
    }
}
 
impl<T> Drop for MyVec<T> {
    fn drop(&mut self) {
        if self.capacity > 0 {
            // Drop all elements
            for i in 0..self.len {
                unsafe {
                    std::ptr::drop_in_place(self.ptr.add(i));
                }
            }
            // Deallocate
            let layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
            unsafe {
                std::alloc::dealloc(self.ptr as *mut u8, layout);
            }
        }
    }
}
 
fn main() {
    let mut vec = MyVec::new();
    vec.push(1);
    vec.push(2);
    vec.push(3);
    
    println!("Length: {}", vec.len());
}

Lazy Initialization

use std::mem::ManuallyDrop;
use std::ptr;
 
struct LazyBuffer {
    // Start uninitialized, then initialize on first use
    data: ManuallyDrop<Vec<u8>>,
    initialized: bool,
}
 
impl LazyBuffer {
    fn new() -> Self {
        Self {
            // Safe because we check initialized before use
            data: unsafe { std::mem::uninitialized() },
            initialized: false,
        }
    }
    
    fn get(&mut self) -> &Vec<u8> {
        if !self.initialized {
            // Initialize on first access
            self.data = ManuallyDrop::new(vec![0; 1024]);
            self.initialized = true;
        }
        &self.data
    }
}
 
impl Drop for LazyBuffer {
    fn drop(&mut self) {
        if self.initialized {
            unsafe {
                ManuallyDrop::drop(&mut self.data);
            }
        }
        // If not initialized, don't drop - nothing to clean up
    }
}
 
fn main() {
    let mut buffer = LazyBuffer::new();
    println!("Created but not initialized");
    
    let data = buffer.get();
    println!("Buffer size: {}", data.len());
}

Transfer Ownership to FFI

use std::mem::ManuallyDrop;
 
// Imagine a C function that takes ownership of data
// void c_take_ownership(void* data, size_t len);
 
struct OwnedData {
    data: ManuallyDrop<Vec<u8>>,
}
 
impl OwnedData {
    fn new(data: Vec<u8>) -> Self {
        Self {
            data: ManuallyDrop::new(data),
        }
    }
    
    fn transfer_to_c(self) {
        // Prevent Rust from dropping the data
        let data = ManuallyDrop::into_inner(unsafe { std::ptr::read(&self.data) });
        
        // Now transfer ownership to C
        // C is responsible for freeing this memory
        let ptr = data.as_ptr();
        let len = data.len();
        
        // Forget the Vec so Rust doesn't try to free it
        std::mem::forget(data);
        
        // Call C function (simulated)
        println!("Transferring {} bytes at {:?} to C", len, ptr);
        
        // In real code:
        // unsafe { c_take_ownership(ptr as *mut void, len); }
    }
}
 
fn main() {
    let owned = OwnedData::new(vec![1, 2, 3, 4, 5]);
    owned.transfer_to_c();
}

Avoiding Double Drop

use std::mem::ManuallyDrop;
 
struct Container {
    data: ManuallyDrop<String>,
    should_drop: bool,
}
 
impl Container {
    fn new(s: String) -> Self {
        Self {
            data: ManuallyDrop::new(s),
            should_drop: true,
        }
    }
    
    fn dont_drop(mut self) {
        self.should_drop = false;
        // self won't drop the inner String
    }
}
 
impl Drop for Container {
    fn drop(&mut self) {
        if self.should_drop {
            unsafe {
                ManuallyDrop::drop(&mut self.data);
            }
        }
    }
}
 
fn main() {
    let c1 = Container::new(String::from("will drop"));
    // c1.data will be dropped when c1 goes out of scope
    
    let c2 = Container::new(String::from("won't drop"));
    c2.dont_drop();
    // c2.data will NOT be dropped
    
    println!("Containers handled");
}

ManuallyDrop Methods

use std::mem::ManuallyDrop;
 
fn main() {
    let mut manual = ManuallyDrop::new(String::from("test"));
    
    // Deref to access inner value
    println!("Value: {}", *manual);
    
    // DerefMut to modify
    manual.push_str(" modified");
    println!("Modified: {}", *manual);
    
    // Check size
    println!("Size of ManuallyDrop<String>: {}", std::mem::size_of_val(&manual));
    println!("Size of String: {}", std::mem::size_of::<String>());
    // Same size!
    
    // into_inner consumes and returns the value
    let string = ManuallyDrop::into_inner(manual);
    println!("Extracted: {}", string);
    // string will be dropped normally
    
    // take extracts without consuming ManuallyDrop
    let mut manual2 = ManuallyDrop::new(vec![1, 2, 3]);
    let vec = unsafe { ManuallyDrop::take(&mut manual2) };
    println!("Taken: {:?}", vec);
}

Slot-based Allocation

use std::mem::ManuallyDrop;
 
struct Slot<T> {
    data: ManuallyDrop<T>,
    occupied: bool,
}
 
impl<T> Slot<T> {
    fn new_uninit() -> Self {
        Self {
            data: unsafe { std::mem::zeroed() },
            occupied: false,
        }
    }
    
    fn is_occupied(&self) -> bool {
        self.occupied
    }
    
    fn insert(&mut self, value: T) -> Result<(), T> {
        if self.occupied {
            return Err(value);
        }
        
        self.data = ManuallyDrop::new(value);
        self.occupied = true;
        Ok(())
    }
    
    fn remove(&mut self) -> Option<T> {
        if !self.occupied {
            return None;
        }
        
        self.occupied = false;
        // Take the value out
        Some(unsafe { ManuallyDrop::take(&mut self.data) })
    }
}
 
impl<T> Drop for Slot<T> {
    fn drop(&mut self) {
        if self.occupied {
            unsafe {
                ManuallyDrop::drop(&mut self.data);
            }
        }
    }
}
 
fn main() {
    let mut slot: Slot<String> = Slot::new_uninit();
    
    println!("Occupied: {}", slot.is_occupied());
    
    slot.insert(String::from("hello")).unwrap();
    println!("Occupied: {}", slot.is_occupied());
    
    let value = slot.remove().unwrap();
    println!("Removed: {}", value);
    println!("Occupied: {}", slot.is_occupied());
}

Summary

ManuallyDrop Methods:

Method Description Safety
new(value) Create new ManuallyDrop Safe
into_inner(this) Extract value, consuming wrapper Safe
take(slot) Extract value without consuming Unsafe
drop(slot) Manually drop the inner value Unsafe
deref / deref_mut Access inner value Safe

Comparison with Related Types:

Type Auto Drop Use Case
T Yes Normal values
ManuallyDrop<T> No Manual drop control
MaybeUninit<T> No Uninitialized memory
mem::forget() Prevents drop One-time forget

When to Use ManuallyDrop:

Scenario Appropriate?
Custom smart pointers āœ… Yes
Vec/Collection internals āœ… Yes
FFI ownership transfer āœ… Yes
Unions with non-Copy types āœ… Yes
Avoiding double-drop āœ… Yes
Simple resource cleanup āŒ Use normal Drop
Thread-safe shared state āŒ Use Arc/Mutex

Key Points:

  • ManuallyDrop<T> prevents automatic Drop
  • Same size and layout as T (zero overhead)
  • drop() and take() are unsafe methods
  • Forgetting to drop causes memory leaks
  • Essential for implementing low-level data structures
  • Use into_inner() to safely extract and transfer ownership
  • Combine with MaybeUninit for uninitialized + manual drop control