Reinventing From Scratch — Box<T>
Chapter 12 — Appendix: Full Minimal Impl, FAQ & Glossary
12.1 Full minimal implementation (sized T)
use std::alloc::{alloc, dealloc, Layout};
use std::ops::{Deref, DerefMut};
use std::ptr;
pub struct MyBox<T> { ptr: *mut T }
impl<T> MyBox<T> {
pub fn new(value: T) -> Self {
unsafe {
let p = alloc(Layout::new::<T>()) as *mut T;
if p.is_null() { panic!("allocation failed"); }
ptr::write(p, value);
MyBox { ptr: p }
}
}
#[inline] pub fn as_ptr(&self) -> *const T { self.ptr }
#[inline] pub fn as_mut_ptr(&mut self) -> *mut T { self.ptr }
pub fn into_raw(mut this: Self) -> *mut T {
let p = this.ptr;
this.ptr = std::ptr::null_mut();
std::mem::forget(this);
p
}
/// # Safety
/// `ptr` must originate from `MyBox::into_raw` with the same `T` and allocator.
pub unsafe fn from_raw(ptr: *mut T) -> Self { MyBox { ptr } }
pub fn leak(this: Self) -> &'static mut T {
let p = Self::into_raw(this);
unsafe { &mut *p }
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target { unsafe { &*self.ptr } }
}
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target { unsafe { &mut *self.ptr } }
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
if self.ptr.is_null() { return; }
ptr::drop_in_place(self.ptr);
dealloc(self.ptr.cast(), Layout::new::<T>());
}
}
}
12.2 FAQ
- Does moving
Box<T>moveT? No. Only the pointer moves;Tstays at the same heap address. - When to use
Box<T>vsRc<T>/Arc<T>? UseBoxfor unique ownership;Rc/Arcfor shared. - Do I need
Pin<Box<T>>for a stable address?Boxalready keeps a stable address;Pinalso forbids moves through the type system and API. - Can I write my own DST box? Possible but tricky: you must handle fat pointers and reconstruct
Layoutusing metadata.
12.3 Glossary
- UB (Undefined Behavior): Anything may happen; compiler assumptions are violated.
- Layout: Size + alignment descriptor for allocation.
- ZST: Zero-Sized Type (e.g.,
()); still has identity. - NonNull: Non-null raw pointer wrapper; enables niche optimizations.
- Fat pointer: Pointer + metadata (length or vtable).
Final Exercise
Port this MyBox<T> into a small crate with tests and run under Miri. Add an example that leaks a global configuration and explain why the leak is acceptable.
Deep Dive: Ownership Proofs, Drop Order, and DST Considerations
A. Formal Invariants for MyBox<T> (Sized)
- B1 (Pointer Validity):
ptris either null only afterinto_rawor a valid, properly aligned pointer to initializedT. - B2 (Single Drop): The destructor of
Tis invoked exactly once if and only ifptris non-null atDroptime. - B3 (Dealloc after Drop):
dealloc(layout_of::<T>())is called exactly once, and only afterdrop_in_place. - B4 (From/Into Raw Consistency):
from_rawonly accepts pointers produced byinto_rawof the same type/allocator; mixing allocators is UB. - B5 (No References to Uninit): No
&/&mutreferences are created beforeptr::writeinitializes the allocation.
B. Proof Sketches
B.1 Single Drop — Drop checks for null and calls drop_in_place once; into_raw nulls out ptr and forgets self, preventing Drop from running on a live value.
B.2 No Use-After-Free — Deallocation happens only after the destructor; references returned by Deref are derived from a live ptr and never stored beyond the box’s lifetime.
B.3 Panic Safety — If constructor panics before publishing, no ownership is established; if Drop panics (should not), process aborts, avoiding double-unwind corruption.
C. DST Box Notes
- Slices (
Box<[T]>): store length; the fat pointer (data, len) enables correct deallocation. Box<str>: same as[u8]with UTF‑8 invariant; length in metadata.Box<dyn Trait>: fat pointer (data, vtable); the vtable encodes drop and size/alignment; std uses compiler magic for correct layout.
D. Interop Patterns
- FFI Ownership Transfer:
into_raw-> C takes ownership; C must call back into Rust withfrom_rawor a custom free. - Leaking Globals:
leakreturns'staticreference, acceptable for process lifetime singletons; document intent.
E. Debugging
- Double Drop: look for
*passignment instead ofptr::writeon uninitialized memory. - Mismatched Layout: using
deallocwith wrongLayoutcauses heap corruption; keepLayout::new::<T>()paired.
F. Exercises
- Implement
try_newreturningResult<MyBox<T>, AllocError>. - Add
into_inner(self) -> Tbyptr::readand skipping dealloc? Explain why you must still dealloc after movingT. - Implement
MyVec::into_boxed_slicethat handsRawVecbuffer to aBox<[T]>safely.
FAQ (Extended)
Q: Does Box<T> guarantee a stable address? A: Yes, the pointee’s address is stable for the life of the box; moving the box moves only the handle.
Q: Why ptr::write not *p = value? A: The latter reads/drops the previous contents (uninitialized), which is UB.
Q: Can Box<T> be null? A: By design, standard Box<T> is non-null; our MyBox may set ptr = null only as a consumed sentinel post-into_raw.
Q: Is Pin<Box<T>> needed for stable address? A: Not for stability; Pin is for forbidding moves via the API.