How do I work with mem::ManuallyDrop in Rust?
Walkthrough
ManuallyDrop is a wrapper type that prevents automatic drop calls. It's used when you need explicit control over when a value is destroyed, such as in FFI, implementing custom smart pointers, or optimizing performance-critical code.
Key concepts:
- No automatic drop ā The inner value won't be dropped automatically
- Unsafe to forget ā You must manually drop or extract the value
- Zero overhead ā Same memory layout as
T - Drop order control ā Explicit control over destruction order
When to use ManuallyDrop:
- Implementing custom smart pointers (Rc, Arc, Box)
- FFI with non-Rust memory management
- Performance optimizations avoiding redundant drops
- Union types (required for safety)
- Transferring ownership to another location
Safety considerations:
- Forgetting to drop a ManuallyDrop causes memory leaks
- Dropping twice is undefined behavior
- Use
drop_in_placeortakefor safe cleanup
Code Examples
Basic ManuallyDrop Usage
use std::mem::ManuallyDrop;
fn main() {
// Wrap a value in ManuallyDrop
let mut value = ManuallyDrop::new(String::from("Hello, World!"));
println!("Value: {}", value);
// Access the inner value
println!("Length: {}", value.len());
// The value is NOT dropped when 'value' goes out of scope!
// We must manually drop it:
unsafe {
ManuallyDrop::drop(&mut value);
}
// After drop, 'value' must not be used!
}Preventing Automatic Drop
use std::mem::ManuallyDrop;
struct Resource {
name: String,
data: Vec<u8>,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropping resource: {}", self.name);
}
}
fn main() {
{
let _normal = Resource {
name: String::from("normal"),
data: vec![1, 2, 3],
};
// Will be dropped at end of scope
}
println!("---");
{
let _manual = ManuallyDrop::new(Resource {
name: String::from("manual"),
data: vec![4, 5, 6],
});
// Will NOT be dropped at end of scope!
// Memory leak!
}
println!("After manual scope");
}Proper Cleanup with ManuallyDrop
use std::mem::ManuallyDrop;
fn main() {
let mut manual = ManuallyDrop::new(String::from("Important data"));
// Method 1: Use ManuallyDrop::drop
unsafe {
ManuallyDrop::drop(&mut manual);
}
// After this, manual is invalid - don't use it!
// Method 2: Take the value out
let mut manual2 = ManuallyDrop::new(String::from("More data"));
let extracted: String = ManuallyDrop::take(&mut manual2);
println!("Extracted: {}", extracted);
// extracted will be dropped normally
// manual2 is now empty - don't use it!
}Implementing a Smart Pointer
use std::mem::ManuallyDrop;
use std::ops::Deref;
use std::ptr::NonNull;
struct MyBox<T> {
ptr: NonNull<T>,
}
impl<T> MyBox<T> {
fn new(value: T) -> Self {
let boxed = Box::new(value);
Self {
ptr: unsafe { NonNull::new_unchecked(Box::into_raw(boxed)) },
}
}
}
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) {
// Need to manually drop the Box
unsafe {
drop(Box::from_raw(self.ptr.as_ptr()));
}
}
}
// A more complex example with reference counting
struct RcInner<T> {
value: ManuallyDrop<T>,
ref_count: usize,
}
pub struct SimpleRc<T> {
inner: NonNull<RcInner<T>>,
}
impl<T> SimpleRc<T> {
pub fn new(value: T) -> Self {
let inner = Box::new(RcInner {
value: ManuallyDrop::new(value),
ref_count: 1,
});
Self {
inner: unsafe { NonNull::new_unchecked(Box::into_raw(inner)) },
}
}
pub fn get(&self) -> &T {
unsafe { &self.inner.as_ref().value }
}
}
impl<T> Clone for SimpleRc<T> {
fn clone(&self) -> Self {
unsafe {
(*self.inner.as_ptr()).ref_count += 1;
}
Self { inner: self.inner }
}
}
impl<T> Drop for SimpleRc<T> {
fn drop(&mut self) {
unsafe {
let inner = self.inner.as_mut();
inner.ref_count -= 1;
if inner.ref_count == 0 {
// Drop the value first
ManuallyDrop::drop(&mut inner.value);
// Then free the allocation
drop(Box::from_raw(self.inner.as_ptr()));
}
}
}
}
fn main() {
let rc1 = SimpleRc::new(String::from("Hello"));
println!("Value: {}", rc1.get());
let rc2 = rc1.clone();
println!("Cloned value: {}", rc2.get());
// Both will be dropped properly
}FFI with Manual Memory Management
use std::mem::ManuallyDrop;
use std::ffi::CString;
use std::os::raw::c_char;
// Simulated FFI resource
struct FfiBuffer {
ptr: *mut c_char,
size: usize,
}
impl FfiBuffer {
fn new(data: &[u8]) -> Self {
let c_string = CString::new(data).unwrap();
Self {
ptr: c_string.into_raw(),
size: data.len(),
}
}
fn as_bytes(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr as *const u8, self.size) }
}
}
// Wrapper that manages FFI memory
struct ManagedFfiBuffer {
// Use ManuallyDrop to control when we free the FFI resource
buffer: ManuallyDrop<FfiBuffer>,
}
impl ManagedFfiBuffer {
fn new(data: &[u8]) -> Self {
Self {
buffer: ManuallyDrop::new(FfiBuffer::new(data)),
}
}
fn as_bytes(&self) -> &[u8] {
self.buffer.as_bytes()
}
// Transfer ownership to FFI (don't drop on our side)
fn into_raw(mut self) -> *mut c_char {
let ptr = self.buffer.ptr;
// Don't drop the buffer - FFI owns it now
std::mem::forget(self);
ptr
}
}
impl Drop for ManagedFfiBuffer {
fn drop(&mut self) {
unsafe {
// Free the CString
let c_string = CString::from_raw(self.buffer.ptr);
drop(c_string);
// Mark as dropped (optional, for documentation)
ManuallyDrop::drop(&mut self.buffer);
}
}
}
fn main() {
let managed = ManagedFfiBuffer::new(b"Hello FFI");
println!("Buffer: {:?}", managed.as_bytes());
// Will be dropped properly
}Union Types with ManuallyDrop
use std::mem::ManuallyDrop;
// A tagged union using ManuallyDrop
enum Tag {
Int,
Float,
String,
}
union Value {
int: i64,
float: f64,
string: ManuallyDrop<String>, // Must use ManuallyDrop for types with Drop
}
struct TaggedValue {
tag: Tag,
value: Value,
}
impl TaggedValue {
fn int(value: i64) -> Self {
Self {
tag: Tag::Int,
value: Value { int: value },
}
}
fn float(value: f64) -> Self {
Self {
tag: Tag::Float,
value: Value { float: value },
}
}
fn string(s: String) -> Self {
Self {
tag: Tag::String,
value: Value { string: ManuallyDrop::new(s) },
}
}
fn as_int(&self) -> Option<i64> {
match self.tag {
Tag::Int => Some(unsafe { self.value.int }),
_ => None,
}
}
fn as_float(&self) -> Option<f64> {
match self.tag {
Tag::Float => Some(unsafe { self.value.float }),
_ => None,
}
}
fn as_string(&self) -> Option<&String> {
match self.tag {
Tag::String => Some(unsafe { &self.value.string }),
_ => None,
}
}
}
impl Drop for TaggedValue {
fn drop(&mut self) {
unsafe {
match self.tag {
Tag::String => {
ManuallyDrop::drop(&mut self.value.string);
}
_ => {} // Int and Float don't need drop
}
}
}
}
fn main() {
let int_val = TaggedValue::int(42);
let float_val = TaggedValue::float(3.14);
let string_val = TaggedValue::string(String::from("Hello"));
println!("Int: {:?}", int_val.as_int());
println!("Float: {:?}", float_val.as_float());
println!("String: {:?}", string_val.as_string());
}Avoiding Redundant Drops
use std::mem::ManuallyDrop;
// A buffer that might transfer ownership
struct Buffer {
data: Vec<u8>,
}
struct TransferableBuffer {
// Use ManuallyDrop so we can optionally transfer without double-free
buffer: ManuallyDrop<Buffer>,
transferred: bool,
}
impl TransferableBuffer {
fn new(data: Vec<u8>) -> Self {
Self {
buffer: ManuallyDrop::new(Buffer { data }),
transferred: false,
}
}
fn data(&self) -> &[u8] {
&self.buffer.data
}
// Transfer ownership to another owner
fn transfer(mut self) -> Buffer {
self.transferred = true;
// Take the buffer out - we won't drop it
ManuallyDrop::take(&mut self.buffer)
}
}
impl Drop for TransferableBuffer {
fn drop(&mut self) {
// Only drop if not transferred
if !self.transferred {
unsafe {
ManuallyDrop::drop(&mut self.buffer);
}
}
}
}
fn main() {
// Normal usage - dropped at end of scope
{
let buf = TransferableBuffer::new(vec![1, 2, 3]);
println!("Data: {:?}", buf.data());
}
// Transfer ownership
{
let buf = TransferableBuffer::new(vec![4, 5, 6]);
let transferred = buf.transfer();
println!("Transferred data: {:?}", transferred.data);
// transferred is now responsible for cleanup
}
}Delayed Drop Pattern
use std::mem::ManuallyDrop;
use std::collections::VecDeque;
struct DelayedDropper {
pending_drops: VecDeque<ManuallyDrop<Box<dyn std::any::Any>>>,
}
impl DelayedDropper {
fn new() -> Self {
Self {
pending_drops: VecDeque::new(),
}
}
fn schedule_drop<T: 'static>(&mut self, value: T) {
self.pending_drops.push_back(ManuallyDrop::new(Box::new(value)));
}
fn process_one(&mut self) -> bool {
if let Some(mut value) = self.pending_drops.pop_front() {
unsafe {
ManuallyDrop::drop(&mut value);
}
true
} else {
false
}
}
fn process_all(&mut self) {
while self.process_one() {}
}
}
impl Drop for DelayedDropper {
fn drop(&mut self) {
// Clean up any remaining items
self.process_all();
}
}
fn main() {
let mut dropper = DelayedDropper::new();
dropper.schedule_drop(String::from("First"));
dropper.schedule_drop(String::from("Second"));
dropper.schedule_drop(vec![1, 2, 3]);
println!("Processing drops...");
dropper.process_one();
println!("Processed one");
// Remaining items dropped when dropper goes out of scope
}ManuallyDrop with Read/Write
use std::mem::ManuallyDrop;
fn main() {
let mut value = ManuallyDrop::new(String::from("Hello"));
// Read access (safe)
println!("Value: {}", *value);
// Modify the inner value (safe)
value.push_str(", World!");
println!("Modified: {}", *value);
// Replace the inner value
let old = std::mem::replace(&mut *value, String::from("New value"));
println!("Old: {}, New: {}", old, *value);
// old is a normal String and will be dropped
// Clean up
unsafe {
ManuallyDrop::drop(&mut value);
}
}Slot Allocation Pattern
use std::mem::ManuallyDrop;
use std::mem::MaybeUninit;
struct Slot<T> {
value: ManuallyDrop<T>,
occupied: bool,
}
impl<T> Slot<T> {
fn uninit() -> Self {
// This is a placeholder - in real code use MaybeUninit
unimplemented!("Use MaybeUninit for truly uninitialized slots")
}
}
// A better approach using MaybeUninit + ManuallyDrop
struct SafeSlot<T> {
value: MaybeUninit<ManuallyDrop<T>>,
occupied: bool,
}
impl<T> SafeSlot<T> {
fn new() -> Self {
Self {
value: MaybeUninit::uninit(),
occupied: false,
}
}
fn put(&mut self, value: T) {
assert!(!self.occupied, "Slot already occupied");
self.value.write(ManuallyDrop::new(value));
self.occupied = true;
}
fn take(&mut self) -> Option<T> {
if self.occupied {
self.occupied = false;
// SAFETY: We know the slot is occupied
let manually_drop = unsafe { self.value.assume_init_read() };
Some(ManuallyDrop::into_inner(manually_drop))
} else {
None
}
}
fn get(&self) -> Option<&T> {
if self.occupied {
// SAFETY: We know the slot is occupied
Some(unsafe { &*self.value.assume_init_ref() })
} else {
None
}
}
}
impl<T> Drop for SafeSlot<T> {
fn drop(&mut self) {
if self.occupied {
unsafe {
ManuallyDrop::drop(self.value.assume_init_mut());
}
}
}
}
fn main() {
let mut slot = SafeSlot::new();
assert!(slot.get().is_none());
slot.put(String::from("Hello"));
println!("Value: {:?}", slot.get());
let taken = slot.take();
println!("Taken: {:?}", taken);
assert!(slot.get().is_none());
}Summary
ManuallyDrop Methods:
| Method | Description | Safety |
|---|---|---|
new(value) |
Wrap value in ManuallyDrop | Safe |
into_inner(this) |
Extract value, consuming ManuallyDrop | Safe |
take(this) |
Extract value from &mut ManuallyDrop<T> |
Safe |
drop(slot) |
Manually drop the inner value | Unsafe |
Deref / DerefMut |
Access inner value | Safe |
When to Use ManuallyDrop:
| Scenario | Appropriate? |
|---|---|
| Implementing smart pointers | ā Yes |
| FFI with manual memory management | ā Yes |
| Union variants with Drop types | ā Required |
| Transferring ownership | ā Yes |
| Avoiding redundant drops | ā Yes |
| Normal Rust code | ā Use automatic drop |
| When you might forget to drop | ā Risky |
Safety Invariants:
| Action | Safety |
|---|---|
| Creating ManuallyDrop | Safe |
| Reading/writing inner value | Safe |
Calling drop twice |
ā UB (Double drop) |
| Forgetting to drop | ā ļø Memory leak |
Using after drop |
ā UB (Use after free) |
Using after take |
ā UB (Use after move) |
Key Points:
- ManuallyDrop prevents automatic Drop
- Use
ManuallyDrop::drop(&mut slot)to manually drop - Use
ManuallyDrop::take(&mut slot)to extract ownership - Same memory layout as inner type
- Required for union types with Drop impls
- Essential for implementing custom smart pointers
- Memory leak if you forget to drop
- Double drop is undefined behavior
