[{"content":"This post explores how Rust manages memory through Box, Vec, and the global allocator, uncovering what really happens under the hood.\nBox and Vec Memory Allocation Before diving into implementation details, let’s first understand how Box and Vec internally use allocators.\nBox Box is the simplest smart pointer in Rust. It allocates a value on the heap while making ownership explicit and safe.\nUnder the hood, though, Box is more complex than the Box\u0026lt;T\u0026gt; we usually write. Here’s the actual definition:\n1 2 3 4 5 6 7 8 9 10 11 #[lang = \u0026#34;owned_box\u0026#34;] #[fundamental] #[stable(feature = \u0026#34;rust1\u0026#34;, since = \u0026#34;1.0.0\u0026#34;)] #[rustc_insignificant_dtor] #[doc(search_unbox)] // The declaration of the `Box` struct must be kept in sync with the // compiler or ICEs will happen. pub struct Box\u0026lt; T: ?Sized, #[unstable(feature = \u0026#34;allocator_api\u0026#34;, issue = \u0026#34;32838\u0026#34;)] A: Allocator = Global, \u0026gt;(Unique\u0026lt;T\u0026gt;, A); Box has two fields:\nUnique\u0026lt;T\u0026gt; T: ?Sized: Allows Box to hold dynamically sized types like Box\u0026lt;dyn Trait\u0026gt; or Box\u0026lt;[u8]\u0026gt;. Unique\u0026lt;T\u0026gt;: A wrapper around a non-null pointer that guarantees uniqueness, ensuring the Box owns its allocation exclusively. A A: Allocator = Global: Represents the allocator used to manage the memory. By default this is Global, which adds no runtime overhead because it’s a zero-sized type (ZST). How Box::new Works At first glance, Box::new looks like an ordinary function:\n1 2 3 4 5 6 7 8 9 #[cfg(not(no_global_oom_handling))] #[inline(always)] #[stable(feature = \u0026#34;rust1\u0026#34;, since = \u0026#34;1.0.0\u0026#34;)] #[must_use] #[rustc_diagnostic_item = \u0026#34;box_new\u0026#34;] #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces pub fn new(x: T) -\u0026gt; Self { return box_new(x); } But Box::new doesn’t perform allocation itself—it calls the compiler intrinsic box_new.\n1 2 3 #[rustc_intrinsic] #[unstable(feature = \u0026#34;liballoc_internals\u0026#34;, issue = \u0026#34;none\u0026#34;)] pub fn box_new\u0026lt;T\u0026gt;(x: T) -\u0026gt; Box\u0026lt;T\u0026gt;; This intrinsic is lowered by the compiler into a call to exchange_malloc, which is the real allocation entry point:\n1 2 3 4 5 6 7 8 9 10 11 #[cfg(not(no_global_oom_handling))] #[lang = \u0026#34;exchange_malloc\u0026#34;] #[inline] #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces unsafe fn exchange_malloc(size: usize, align: usize) -\u0026gt; *mut u8 { let layout = unsafe { Layout::from_size_align_unchecked(size, align) }; match Global.allocate(layout) { Ok(ptr) =\u0026gt; ptr.as_mut_ptr(), Err(_) =\u0026gt; handle_alloc_error(layout), } } Flow: Box::new → box_new → exchange_malloc\nWhen you compile:\n1 2 3 fn main() { Box::new(true); } What looks like a simple Box::new(true) in Rust is actually lowered by the compiler into a raw call to exchange_malloc.\nThe binary output above clearly shows that Box::new(true) is translated into a call to exchange_malloc — this is where the real allocation takes place.\nHow Box is Dropped When a Box is dropped, the compiler eventually invokes the special lang item drop_in_place. This function is just a placeholder — the compiler replaces it with real drop glue for the type being dropped:\n1 2 3 4 5 6 7 8 9 10 11 #[stable(feature = \u0026#34;drop_in_place\u0026#34;, since = \u0026#34;1.8.0\u0026#34;)] #[lang = \u0026#34;drop_in_place\u0026#34;] #[allow(unconditional_recursion)] #[rustc_diagnostic_item = \u0026#34;ptr_drop_in_place\u0026#34;] pub unsafe fn drop_in_place\u0026lt;T: PointeeSized\u0026gt;(to_drop: *mut T) { // Code here does not matter - this is replaced by the // real drop glue by the compiler. // SAFETY: see comment above unsafe { drop_in_place(to_drop) } } At first glance, this looks like infinite recursion. But the trick is:\nThe function body is just a stub. The #[lang = \u0026quot;drop_in_place\u0026quot;] attribute tells the compiler to handle it specially. During compilation, the compiler generates real code that knows how to properly drop values of type T. For Box, this placeholder gets replaced with its actual Drop implementation:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #[stable(feature = \u0026#34;rust1\u0026#34;, since = \u0026#34;1.0.0\u0026#34;)] unsafe impl\u0026lt;#[may_dangle] T: ?Sized, A: Allocator\u0026gt; Drop for Box\u0026lt;T, A\u0026gt; { #[inline] fn drop(\u0026amp;mut self) { // the T in the Box is dropped by the compiler before the destructor is run let ptr = self.0; unsafe { let layout = Layout::for_value_raw(ptr.as_ptr()); if layout.size() != 0 { self.1.deallocate(From::from(ptr.cast()), layout); } } } } In other words:\nThe compiler inserts drop glue to drop the inner value T. Then, the Box destructor calls the allocator’s deallocate method. With the default allocator (Global), this eventually frees the memory back to the system. Vec Vec is a growable, heap-allocated, contiguous array type. Unlike arrays or slices, it owns its buffer and can resize itself as elements are pushed or popped.\nHere’s the actual definition:\n1 2 3 4 5 6 7 #[stable(feature = \u0026#34;rust1\u0026#34;, since = \u0026#34;1.0.0\u0026#34;)] #[rustc_diagnostic_item = \u0026#34;Vec\u0026#34;] #[rustc_insignificant_dtor] pub struct Vec\u0026lt;T, #[unstable(feature = \u0026#34;allocator_api\u0026#34;, issue = \u0026#34;32838\u0026#34;)] A: Allocator = Global\u0026gt; { buf: RawVec\u0026lt;T, A\u0026gt;, len: usize, } Breaking Down the Fields\nbuf: RawVec\u0026lt;T, A\u0026gt; The underlying buffer that manages the heap allocation. RawVec stores a pointer to the allocated memory and keeps track of the current capacity. It is generic over an allocator A (default: Global), which makes Vec compatible with custom allocators. len: usize The number of initialized elements in the vector. Must always be less than or equal to the buffer’s capacity. When elements are pushed or popped, len changes, but the buffer itself is not necessarily reallocated immediately. How Vec::new Works 1 2 3 4 5 6 7 8 #[inline] #[rustc_const_stable(feature = \u0026#34;const_vec_new\u0026#34;, since = \u0026#34;1.39.0\u0026#34;)] #[rustc_diagnostic_item = \u0026#34;vec_new\u0026#34;] #[stable(feature = \u0026#34;rust1\u0026#34;, since = \u0026#34;1.0.0\u0026#34;)] #[must_use] pub const fn new() -\u0026gt; Self { Vec { buf: RawVec::new(), len: 0 } } Under the hood, Vec::new constructs a RawVec, which in turn wraps RawVecInner:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #[allow(missing_debug_implementations)] pub(crate) struct RawVec\u0026lt;T, A: Allocator = Global\u0026gt; { inner: RawVecInner\u0026lt;A\u0026gt;, _marker: PhantomData\u0026lt;T\u0026gt;, } impl\u0026lt;T\u0026gt; RawVec\u0026lt;T, Global\u0026gt; { #[must_use] pub(crate) const fn new() -\u0026gt; Self { Self::new_in(Global) } // ... } impl\u0026lt;T, A: Allocator\u0026gt; RawVec\u0026lt;T, A\u0026gt; { #[inline] pub(crate) const fn new_in(alloc: A) -\u0026gt; Self { Self { inner: RawVecInner::new_in(alloc, Alignment::of::\u0026lt;T\u0026gt;()), _marker: PhantomData } } // ... } RawVec has two fields:\ninner: RawVecInner\u0026lt;A\u0026gt; – stores the pointer and capacity, and uses the allocator A (usually Global) to manage memory. _marker: PhantomData\u0026lt;T\u0026gt; – a zero-sized type that informs the compiler that RawVec logically owns values of type T. This ensures correct drop-check behavior and variance rules, even though no T is stored directly. About RawVecInner The real allocation logic lives inside RawVecInner:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #[allow(missing_debug_implementations)] struct RawVecInner\u0026lt;A: Allocator = Global\u0026gt; { ptr: Unique\u0026lt;u8\u0026gt;, cap: Cap, alloc: A, } impl\u0026lt;A: Allocator\u0026gt; RawVecInner\u0026lt;A\u0026gt; { #[inline] const fn new_in(alloc: A, align: Alignment) -\u0026gt; Self { let ptr = Unique::from_non_null(NonNull::without_provenance(align.as_nonzero())); // `cap: 0` means \u0026#34;unallocated\u0026#34;. zero-sized types are ignored. Self { ptr, cap: ZERO_CAP, alloc } } } It stores:\nptr – raw pointer to the allocated buffer. cap – the current capacity. alloc – the allocator instance that owns the memory. In short: RawVec is the interface, while RawVecInner is the engine that handles allocation, growth, and deallocation.\nWhen created with new_in, no memory is allocated immediately. Instead, it sets up a valid, aligned dummy pointer with capacity = 0. Real allocation happens only when the vector actually needs space.\nHow Vec::push Works Appending with push looks simple, but involves a clever growth strategy:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #[cfg(not(no_global_oom_handling))] #[inline] #[stable(feature = \u0026#34;rust1\u0026#34;, since = \u0026#34;1.0.0\u0026#34;)] #[rustc_confusables(\u0026#34;push_back\u0026#34;, \u0026#34;put\u0026#34;, \u0026#34;append\u0026#34;)] #[track_caller] pub fn push(\u0026amp;mut self, value: T) { // Inform codegen that the length does not change across grow_one(). let len = self.len; // This will panic or abort if we would allocate \u0026gt; isize::MAX bytes // or if the length increment would overflow for zero-sized types. if len == self.buf.capacity() { self.buf.grow_one(); } unsafe { let end = self.as_mut_ptr().add(len); ptr::write(end, value); self.len = len + 1; } } Steps:\nSave the current length. If len == capacity, call grow_one to make space. Compute the pointer to the end of the buffer. Write the element into memory. Increment len. How Growth Happens If the buffer is full, push delegates to RawVec::grow_one, which calls RawVecInner::grow_amortized:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 impl\u0026lt;A: Allocator\u0026gt; RawVecInner\u0026lt;A\u0026gt; { // ... #[cfg(not(no_global_oom_handling))] #[inline] #[track_caller] fn grow_one(\u0026amp;mut self, elem_layout: Layout) { if let Err(err) = self.grow_amortized(self.cap.as_inner(), 1, elem_layout) { handle_error(err); } } fn grow_amortized( \u0026amp;mut self, len: usize, additional: usize, elem_layout: Layout, ) -\u0026gt; Result\u0026lt;(), TryReserveError\u0026gt; { // This is ensured by the calling contexts. debug_assert!(additional \u0026gt; 0); if elem_layout.size() == 0 { // Since we return a capacity of `usize::MAX` when `elem_size` is // 0, getting to here necessarily means the `RawVec` is overfull. return Err(CapacityOverflow.into()); } // Nothing we can really do about these checks, sadly. let required_cap = len.checked_add(additional).ok_or(CapacityOverflow)?; // This guarantees exponential growth. The doubling cannot overflow // because `cap \u0026lt;= isize::MAX` and the type of `cap` is `usize`. let cap = cmp::max(self.cap.as_inner() * 2, required_cap); let cap = cmp::max(min_non_zero_cap(elem_layout.size()), cap); let new_layout = layout_array(cap, elem_layout)?; let ptr = finish_grow(new_layout, self.current_memory(elem_layout), \u0026amp;mut self.alloc)?; // SAFETY: finish_grow would have resulted in a capacity overflow if we tried to allocate more than `isize::MAX` items unsafe { self.set_ptr_and_cap(ptr, cap) }; Ok(()) } // ... } Key ideas:\nHandle zero-sized types (ZSTs) Separately. Calculate the required capacity. Double the capacity (amortized growth). Compute a new layout. Allocate a larger buffer. Update the pointer and capacity. The finish_grow Step This is where actual memory allocation (or reallocation) occurs:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #[cold] fn finish_grow\u0026lt;A\u0026gt;( new_layout: Layout, current_memory: Option\u0026lt;(NonNull\u0026lt;u8\u0026gt;, Layout)\u0026gt;, alloc: \u0026amp;mut A, ) -\u0026gt; Result\u0026lt;NonNull\u0026lt;[u8]\u0026gt;, TryReserveError\u0026gt; where A: Allocator, { alloc_guard(new_layout.size())?; let memory = if let Some((ptr, old_layout)) = current_memory { debug_assert_eq!(old_layout.align(), new_layout.align()); unsafe { // The allocator checks for alignment equality hint::assert_unchecked(old_layout.align() == new_layout.align()); alloc.grow(ptr, old_layout, new_layout) } } else { alloc.allocate(new_layout) }; memory.map_err(|_| AllocError { layout: new_layout, non_exhaustive: () }.into()) } // library/core/src/alloc/mod.rs #[unstable(feature = \u0026#34;allocator_api\u0026#34;, issue = \u0026#34;32838\u0026#34;)] pub unsafe trait Allocator { // ... unsafe fn grow( \u0026amp;self, ptr: NonNull\u0026lt;u8\u0026gt;, old_layout: Layout, new_layout: Layout, ) -\u0026gt; Result\u0026lt;NonNull\u0026lt;[u8]\u0026gt;, AllocError\u0026gt; { debug_assert!( new_layout.size() \u0026gt;= old_layout.size(), \u0026#34;`new_layout.size()` must be greater than or equal to `old_layout.size()`\u0026#34; ); let new_ptr = self.allocate(new_layout)?; // SAFETY: because `new_layout.size()` must be greater than or equal to // `old_layout.size()`, both the old and new memory allocation are valid for reads and // writes for `old_layout.size()` bytes. Also, because the old allocation wasn\u0026#39;t yet // deallocated, it cannot overlap `new_ptr`. Thus, the call to `copy_nonoverlapping` is // safe. The safety contract for `dealloc` must be upheld by the caller. unsafe { ptr::copy_nonoverlapping(ptr.as_ptr(), new_ptr.as_mut_ptr(), old_layout.size()); self.deallocate(ptr, old_layout); } Ok(new_ptr) } // ... } If the vector already has a buffer, the allocator tries to grow it. Otherwise, it simply allocates a fresh buffer. The Allocator::grow default implementation does three things:\nAllocate a larger buffer. Copy existing elements into it. Free the old buffer. Summary Vec keeps memory safe by:\nGrowing its buffer when more space is required. Copying elements safely into new buffers. Freeing old allocations correctly. push may trigger growth, while pop simply decreases len without changing the underlying allocation.\nGlobal Allocator \u0026amp; Low-level Allocation Functions In this section, let’s take a closer look at how the Global allocator works and how it interacts with Rust’s low-level allocation functions.\nDefinition of Global 1 2 3 4 5 #[unstable(feature = \u0026#34;allocator_api\u0026#34;, issue = \u0026#34;32838\u0026#34;)] #[derive(Copy, Clone, Default, Debug)] // the compiler needs to know when a Box uses the global allocator vs a custom one #[lang = \u0026#34;global_alloc_ty\u0026#34;] pub struct Global; Global is defined as a zero-sized type (ZST). As noted earlier, it implements the Allocator trait and serves as the default allocator for Box, Vec, String, and other standard collections.\nImplementing the Allocator Trait The Allocator trait provides a uniform interface for allocation, deallocation, and reallocation, which allows collections to stay allocator-agnostic. Global implements it like this:\n1 2 3 4 5 6 7 8 9 #[unstable(feature = \u0026#34;allocator_api\u0026#34;, issue = \u0026#34;32838\u0026#34;)] unsafe impl Allocator for Global { #[inline] #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces fn allocate(\u0026amp;self, layout: Layout) -\u0026gt; Result\u0026lt;NonNull\u0026lt;[u8]\u0026gt;, AllocError\u0026gt; { self.alloc_impl(layout, false) } // ... } Here, allocate simply forwards to alloc_impl.\nThe Core Allocation Logic The real work happens inside alloc_impl, which delegates to the underlying system allocator:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 impl Global { #[inline] #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces fn alloc_impl(\u0026amp;self, layout: Layout, zeroed: bool) -\u0026gt; Result\u0026lt;NonNull\u0026lt;[u8]\u0026gt;, AllocError\u0026gt; { match layout.size() { 0 =\u0026gt; Ok(NonNull::slice_from_raw_parts(layout.dangling(), 0)), // SAFETY: `layout` is non-zero in size, size =\u0026gt; unsafe { let raw_ptr = if zeroed { alloc_zeroed(layout) } else { alloc(layout) }; let ptr = NonNull::new(raw_ptr).ok_or(AllocError)?; Ok(NonNull::slice_from_raw_parts(ptr, size)) }, } } // ... } Breaking it down:\nZero-sized allocations: If the requested Layout has size 0, Rust doesn’t actually allocate memory. Instead, it returns a dangling pointer that is guaranteed never to be dereferenced. This keeps the API consistent without wasting memory. Non-zero allocations: For real allocations, it calls either alloc(layout) or alloc_zeroed(layout) depending on whether zero-initialization is required. Safety \u0026amp; error handling: The result is wrapped in NonNull, which guarantees the pointer is not null. If the system allocator fails, AllocError is returned instead. The Alloc Shim 1 2 3 4 5 6 7 8 9 10 11 12 13 #[stable(feature = \u0026#34;global_alloc\u0026#34;, since = \u0026#34;1.28.0\u0026#34;)] #[must_use = \u0026#34;losing the pointer will leak memory\u0026#34;] #[inline] #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces pub unsafe fn alloc(layout: Layout) -\u0026gt; *mut u8 { unsafe { // Make sure we don\u0026#39;t accidentally allow omitting the allocator shim in // stable code until it is actually stabilized. __rust_no_alloc_shim_is_unstable_v2(); __rust_alloc(layout.size(), layout.align()) } } This function is deliberately tiny:\nIt’s a stable façade over the allocator ABI. The guard __rust_no_alloc_shim_is_unstable_v2() prevents bypassing the ABI contract. The real work happens inside the compiler-provided symbol __rust_alloc. In other words: alloc doesn’t call the OS directly — it forwards the request to __rust_alloc.\nWhere Does __rust_alloc Come From? Inside liballoc, these entry points are declared as extern \u0026quot;Rust\u0026quot; symbols:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 unsafe extern \u0026#34;Rust\u0026#34; { #[rustc_allocator] #[rustc_nounwind] #[rustc_std_internal_symbol] fn __rust_alloc(size: usize, align: usize) -\u0026gt; *mut u8; #[rustc_deallocator] #[rustc_nounwind] #[rustc_std_internal_symbol] fn __rust_dealloc(ptr: *mut u8, size: usize, align: usize); #[rustc_reallocator] #[rustc_nounwind] #[rustc_std_internal_symbol] fn __rust_realloc(ptr: *mut u8, old_size: usize, align: usize, new_size: usize) -\u0026gt; *mut u8; #[rustc_allocator_zeroed] #[rustc_nounwind] #[rustc_std_internal_symbol] fn __rust_alloc_zeroed(size: usize, align: usize) -\u0026gt; *mut u8; #[rustc_nounwind] #[rustc_std_internal_symbol] fn __rust_no_alloc_shim_is_unstable_v2(); } How are these satisfied?\nDefault Path (no custom allocator) If no #[global_allocator] is provided, Rust wires these symbols to internal shims (__rdl_*) that simply forward to the system allocator:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #[cfg(not(test))] #[doc(hidden)] #[allow(unused_attributes)] #[unstable(feature = \u0026#34;alloc_internals\u0026#34;, issue = \u0026#34;none\u0026#34;)] pub mod __default_lib_allocator { use super::{GlobalAlloc, Layout, System}; #[rustc_std_internal_symbol] pub unsafe extern \u0026#34;C\u0026#34; fn __rdl_alloc(size: usize, align: usize) -\u0026gt; *mut u8 { // SAFETY: see the guarantees expected by `Layout::from_size_align` and // `GlobalAlloc::alloc`. unsafe { let layout = Layout::from_size_align_unchecked(size, align); System.alloc(layout) } } #[rustc_std_internal_symbol] pub unsafe extern \u0026#34;C\u0026#34; fn __rdl_dealloc(ptr: *mut u8, size: usize, align: usize) { // SAFETY: see the guarantees expected by `Layout::from_size_align` and // `GlobalAlloc::dealloc`. unsafe { System.dealloc(ptr, Layout::from_size_align_unchecked(size, align)) } } #[rustc_std_internal_symbol] pub unsafe extern \u0026#34;C\u0026#34; fn __rdl_realloc( ptr: *mut u8, old_size: usize, align: usize, new_size: usize, ) -\u0026gt; *mut u8 { // SAFETY: see the guarantees expected by `Layout::from_size_align` and // `GlobalAlloc::realloc`. unsafe { let old_layout = Layout::from_size_align_unchecked(old_size, align); System.realloc(ptr, old_layout, new_size) } } #[rustc_std_internal_symbol] pub unsafe extern \u0026#34;C\u0026#34; fn __rdl_alloc_zeroed(size: usize, align: usize) -\u0026gt; *mut u8 { // SAFETY: see the guarantees expected by `Layout::from_size_align` and // `GlobalAlloc::alloc_zeroed`. unsafe { let layout = Layout::from_size_align_unchecked(size, align); System.alloc_zeroed(layout) } } } So by default, Box, Vec, and String use the platform allocator.\nCustom Global Allocator If you declare your own type implementing GlobalAlloc and mark it with #[global_allocator], the compiler rewires the __rust_* symbols to call into your implementation:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 use std::alloc::{GlobalAlloc, Layout, System}; struct CustomAllocator; unsafe impl GlobalAlloc for CustomAllocator { unsafe fn alloc(\u0026amp;self, layout: Layout) -\u0026gt; *mut u8 { let ptr = System.alloc(layout); // ... ptr } unsafe fn dealloc(\u0026amp;self, ptr: *mut u8, layout: Layout) { System.dealloc(ptr, layout); // ... } } #[global_allocator] static GLOBAL: CustomAllocator = CustomAllocator; Now all allocations in your program flow through CustomAllocator.\nExample: Logging Allocations Here’s a complete example of a custom allocator that logs every allocation and deallocation:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 use std::alloc::{GlobalAlloc, Layout, System}; struct CustomAllocator; unsafe impl GlobalAlloc for CustomAllocator { unsafe fn alloc(\u0026amp;self, layout: Layout) -\u0026gt; *mut u8 { let ptr = System.alloc(layout); log_alloc(b\u0026#34;Alloc\u0026#34;, layout.size(), layout.align()); ptr } unsafe fn dealloc(\u0026amp;self, ptr: *mut u8, layout: Layout) { System.dealloc(ptr, layout); log_alloc(b\u0026#34;Dealloc\u0026#34;, layout.size(), layout.align()); } } #[global_allocator] static GLOBAL: CustomAllocator = CustomAllocator; fn log_line(msg: \u0026amp;str) { unsafe { libc::write(2, msg.as_ptr() as *const _, msg.len()); libc::write(2, b\u0026#34;\\n\u0026#34;.as_ptr() as *const _, 1); } } fn log_alloc(event: \u0026amp;[u8], size: usize, align: usize) { let mut buf = [0u8; 128]; let mut cursor = 0; // event buf[cursor..cursor+event.len()].copy_from_slice(event); cursor += event.len(); buf[cursor..cursor+6].copy_from_slice(b\u0026#34; size=\u0026#34;); cursor += 7; cursor += itoa_noalloc(size, \u0026amp;mut buf[cursor..]); buf[cursor..cursor+7].copy_from_slice(b\u0026#34; align=\u0026#34;); cursor += 7; cursor += itoa_noalloc(align, \u0026amp;mut buf[cursor..]); buf[cursor] = b\u0026#39;\\n\u0026#39;; cursor += 1; unsafe { libc::write(2, buf.as_ptr() as *const _, cursor); } } fn itoa_noalloc(mut n: usize, out: \u0026amp;mut [u8]) -\u0026gt; usize { let mut tmp = [0u8; 20]; let mut i = tmp.len(); if n == 0 { i -= 1; tmp[i] = b\u0026#39;0\u0026#39;; } else { while n \u0026gt; 0 { i -= 1; tmp[i] = b\u0026#39;0\u0026#39; + (n % 10) as u8; n /= 10; } } let len = tmp.len() - i; out[..len].copy_from_slice(\u0026amp;tmp[i..]); len } fn main() { log_line(\u0026#34;=== main start ===\u0026#34;); let _v = vec![1, 2, 3]; let _s = String::from(\u0026#34;hello\u0026#34;); let _b = Box::new([0u8; 64]); drop(_v); drop(_s); drop(_b); log_line(\u0026#34;=== main end ===\u0026#34;); } Logging is done directly via libc::write to stderr, ensuring no extra allocations are introduced (which could cause recursion).\nSample Output\nAlloc size=4 align=1 Alloc size=64 align=8 Alloc size=456 align=8 === main start === Alloc size=12 align=4 Alloc size=5 align=1 Alloc size=64 align=1 Dealloc size=12 align=4 Dealloc size=5 align=1 Dealloc size=64 align=1 === main end === Dealloc size=4 align=1 Notes\nThe first allocations happen before main (runtime setup like panic hooks and I/O locks). align values reflect type layouts (Vec\u0026lt;i32\u0026gt; needs align=4, String needs align=1, etc.). Because logging avoids heap allocations, it’s safe to run inside the allocator itself. New Allocator trait Stable Status The Allocator trait is still unstable (#[unstable(feature = \u0026quot;allocator_api\u0026quot;, issue = \u0026quot;32838\u0026quot;)]). This means that while collections such as Vec, Box, and String are internally designed to work with custom allocators, the public API for using them with your own allocator is not yet stabilized.\nBasic Struct defined Collections like Vec, Box, and String already store an allocator internally. However, the API that lets developers explicitly provide a custom allocator is still only available on nightly Rust. For example:\n1 let v = Vec::new_in(CustomAllocator); Looking Ahead Rust’s memory allocation is safe by default, but when combined with unsafe it gives you almost as much freedom as C. Beyond the standard system allocator, there are also high-performance alternatives like jemalloc and mimalloc. In future posts, we’ll take a closer look at these allocators, explore how to integrate them into Rust projects, and see what kind of performance differences they can make.\n","permalink":"https://mns0327.github.io/posts/allocate/","summary":"\u003cp\u003eThis post explores how Rust manages memory through \u003ccode\u003eBox\u003c/code\u003e, \u003ccode\u003eVec\u003c/code\u003e, and the global allocator, uncovering what really happens under the hood.\u003c/p\u003e\n\u003ch1 id=\"box-and-vec-memory-allocation\"\u003eBox and Vec Memory Allocation\u003c/h1\u003e\n\u003cp\u003eBefore diving into implementation details, let’s first understand how \u003ccode\u003eBox\u003c/code\u003e and \u003ccode\u003eVec\u003c/code\u003e internally use allocators.\u003c/p\u003e\n\u003ch2 id=\"box\"\u003eBox\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eBox\u003c/code\u003e is the simplest smart pointer in Rust. It allocates a value on the heap while making ownership explicit and safe.\u003c/p\u003e\n\u003cp\u003eUnder the hood, though, \u003ccode\u003eBox\u003c/code\u003e is more complex than the \u003ccode\u003eBox\u0026lt;T\u0026gt;\u003c/code\u003e we usually write. Here’s the actual definition:\u003c/p\u003e","title":"Memory Allocation in Rust: Box, Vec, and the Global Allocator"},{"content":"Most people just use lifetimes without understanding how they are implemented under the hood. So I decided to dig into the compiled binary to see how Rust actually manages lifetimes at the machine level.\nThe String Struct Layout Before diving into the assembly, it helps to recall how Rust represents a String internally. A String is essentially a wrapper around a Vec\u0026lt;u8\u0026gt;, which in turn manages a heap-allocated buffer:\nField Description ptr Pointer to the heap-allocated buffer cap Capacity (number of bytes the buffer can hold without reallocating) len Current length (number of bytes used) alloc/src/string.rs alloc/src/vec/mod.rs alloc/src/raw_vec/mod.rs Move value In Rust, ownership ensures that when a value is passed to a function by move, the caller loses access to it. This guarantees that memory can be safely freed once the function finishes. Unlike in C, where the programmer decides when to free memory, Rust enforces this through the type system.\nHere is a simple example that moves a String value into the f function.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // src/main.rs // This function is marked #[inline(never)] and uses black_box to prevent // the compiler from optimizing it away. Purpose: keep the function visible // in the final binary for studying move semantics in assembly. #[inline(never)] fn f(value: String) { std::hint::black_box(value); } fn main() { let value = String::from(\u0026#34;aaaabbbbccccdddd\u0026#34;); f(value); } Let\u0026rsquo;s look at the main function first.\nFunction main 1 testRust::main: 2 sub rsp, 24 3 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc35___rust_no_alloc_shim_is_unstable_v2@GOTPCREL] 4 mov edi, 16 5 mov esi, 1 6 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc12___rust_alloc@GOTPCREL] 7 test rax, rax 8 je .LBB5_2 9 movups xmm0, xmmword, ptr, [rip, +, .Lanon.d3c4e510eecd334b8939fa13e4b98187.3] 10 movups xmmword, ptr, [rax], xmm0 11 mov qword, ptr, [rsp], 16 12 mov qword, ptr, [rsp, +, 8], rax 13 mov qword, ptr, [rsp, +, 16], 16 14 mov rdi, rsp 15 call testRust::f 16 add rsp, 24 17 ret 18 .LBB5_2: 19 lea rdx, [rip, +, .Lanon.d3c4e510eecd334b8939fa13e4b98187.2] 20 mov edi, 1 21 mov esi, 16 22 call qword, ptr, [rip, +, _ZN5alloc7raw_vec12handle_error17h5c9e72494d298ff8E@GOTPCREL] Memory location 4-6 line: Allocate 16 bytes of memory like malloc 7-8 line: Check rax If rax == 0 -\u0026gt; allocation failed -\u0026gt; jump to error handler Load string 9-10: load 16 bytes of data from .Lanon...5 (String) Store into allocated memory Calling f Function 11-13 line: Prepare arguments for f [rsp] = 16 [rsp+8] = rax [rsp+16] = 16 14-15 line: Call f function 16-17 line: Finish f function Function F 1 testRust::f: 2 sub rsp, 24 3 mov rax, qword, ptr, [rdi, +, 16] 4 mov qword, ptr, [rsp, +, 16], rax 5 movups xmm0, xmmword, ptr, [rdi] 6 movaps xmmword, ptr, [rsp], xmm0 7 mov rax, rsp 8 #APP 9 #NO_APP 10 mov rsi, qword, ptr, [rsp] 11 test rsi, rsi 12 je .LBB4_2 13 mov rdi, qword, ptr, [rsp, +, 8] 14 mov edx, 1 15 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc14___rust_dealloc@GOTPCREL] 16 .LBB4_2: 17 add rsp, 24 18 ret Function Prologue Line 3-6: Copy the parameter. Prepare for Deallocation Line 10-12: Check Deallocate Size rsi size for the deallocate If it\u0026rsquo;s zero → skip deallocation, else proceed to free memory Call Deallocator Line 13-15: rdi = [rsp+8] pointer for the deallocate edx = 1 = align for the deallocate Call dealloc function to free the memory Follow the Stream Now we can see how this String value is moved:\nAllocate 16 bytes memory. Load the string literal from .Lanon...5. store the string into the allocated memory. Pass ownership into f. At the end of f, the string is deallocated. allocate → copy literal → pass to f → drop\nIn this case, the String value is moved into the function, and ownership is dropped inside the function.\nBorrow value Borrowing in Rust means the function only receives a reference, not the ownership of the value. The compiler ensures that the borrowed value will not be dropped while it is still borrowed. In our assembly view, you can see that the f function doesn’t deallocate memory — the deallocation happens when main finishes, because the owner is still main.\nHere is a simple example where a String value is borrowed by the f function.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // src/main.rs // This function is marked #[inline(never)] and uses black_box to prevent // the compiler from optimizing it away. Purpose: keep the function visible // in the final binary for studying move semantics in assembly. #[inline(never)] fn f(value: \u0026amp;String) { std::hint::black_box(value); } fn main() { let value = String::from(\u0026#34;aaaabbbbccccdddd\u0026#34;); f(\u0026amp;value); } Function main 1 testRust::main: 2 push r14 3 push rbx 4 sub rsp, 24 5 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc35___rust_no_alloc_shim_is_unstable_v2@GOTPCREL] 6 mov edi, 16 7 mov esi, 1 8 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc12___rust_alloc@GOTPCREL] 9 test rax, rax 10 je .LBB5_4 11 mov rbx, rax 12 movups xmm0, xmmword, ptr, [rip, +, .Lanon.d3c4e510eecd334b8939fa13e4b98187.3] 13 movups xmmword, ptr, [rax], xmm0 14 mov qword, ptr, [rsp], 16 15 mov qword, ptr, [rsp, +, 8], rax 16 mov qword, ptr, [rsp, +, 16], 16 17 mov rdi, rsp 18 call testRust::f 19 mov esi, 16 20 mov edx, 1 21 mov rdi, rbx 22 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc14___rust_dealloc@GOTPCREL] 23 add rsp, 24 24 pop rbx 25 pop r14 26 ret 27 .LBB5_4: 28 lea rdx, [rip, +, .Lanon.d3c4e510eecd334b8939fa13e4b98187.2] 29 mov edi, 1 30 mov esi, 16 31 call qword, ptr, [rip, +, _ZN5alloc7raw_vec12handle_error17h5c9e72494d298ff8E@GOTPCREL] 32 .LBB5_3: 33 mov r14, rax 34 mov esi, 16 35 mov edx, 1 36 mov rdi, rbx 37 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc14___rust_dealloc@GOTPCREL] 38 mov rdi, r14 39 call _Unwind_Resume Function f 1 testRust::f: 2 mov qword, ptr, [rsp, -, 8], rdi 3 lea rax, [rsp, -, 8] 4 #APP 5 #NO_APP 6 ret As you can see, the contents of main do not change much, and even the call to f looks the same. But inside f, the assembly shows almost nothing happening — it behaves like a dummy function. Borrowing essentially works by copying a pointer into the function. Once the function returns, the original owner (main) is still responsible for deallocating the value at the end of its scope.\nallocate → pass pointer to f → f uses pointer → original caller drops\nIf you compare this with C, the generated binary looks familiar: memory is allocated (similar to malloc) and later freed (similar to free). This is because Rust uses LLVM under the hood, so the low-level instructions resemble C. The crucial difference is when deallocation happens. In C, the programmer must remember to free manually, which can easily cause memory leaks or double frees. Rust, however, enforces ownership rules at compile time, guaranteeing that every allocated value is freed exactly once.\nLooking Ahead These topics will give us a broader perspective on how Rust enforces memory safety — not just in simple cases, but across the wide range of real-world scenarios developers face.\n","permalink":"https://mns0327.github.io/posts/compiler-lifetime/","summary":"\u003cp\u003eMost people just use lifetimes without understanding how they are implemented under the hood. So I decided to dig into the compiled binary to see how Rust actually manages lifetimes at the machine level.\u003c/p\u003e\n\u003ch4 id=\"the-string-struct-layout\"\u003eThe String Struct Layout\u003c/h4\u003e\n\u003cp\u003eBefore diving into the assembly, it helps to recall how Rust represents a \u003ccode\u003eString\u003c/code\u003e internally. A \u003ccode\u003eString\u003c/code\u003e is essentially a wrapper around a \u003ccode\u003eVec\u0026lt;u8\u0026gt;\u003c/code\u003e, which in turn manages a heap-allocated buffer:\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003eField\u003c/th\u003e\n          \u003cth\u003eDescription\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eptr\u003c/td\u003e\n          \u003ctd\u003ePointer to the heap-allocated buffer\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003ecap\u003c/td\u003e\n          \u003ctd\u003eCapacity (number of bytes the buffer can hold without reallocating)\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003elen\u003c/td\u003e\n          \u003ctd\u003eCurrent length (number of bytes used)\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/rust-lang/rust/blob/master/library/alloc/src/string.rs\"\u003ealloc/src/string.rs\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/rust-lang/rust/blob/master/library/alloc/src/vec/mod.rs\"\u003ealloc/src/vec/mod.rs\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/rust-lang/rust/blob/master/library/alloc/src/raw_vec/mod.rs\"\u003ealloc/src/raw_vec/mod.rs\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"move-value\"\u003eMove value\u003c/h2\u003e\n\u003cp\u003eIn Rust, ownership ensures that when a value is passed to a function by move, the caller loses access to it. This guarantees that memory can be safely freed once the function finishes. Unlike in C, where the programmer decides when to free memory, Rust enforces this through the type system.\u003c/p\u003e","title":"Rust: How the Compiler Manages Lifetimes"}]