Mojo Manual Memory
Manual memory management means your code explicitly controls when memory is allocated and when it is released. Mojo's ownership system handles most cleanup automatically, but for maximum performance or C interoperability, you sometimes allocate memory manually using UnsafePointer and manage it yourself.
Stack vs Heap
Stack Heap
───────────────────── ──────────────────────────
Fixed size, fast access Flexible size, slightly slower
Automatically managed Manually managed (or GC)
Lives until scope ends Lives until explicitly freed
Small data fits here Large or dynamic data goes here
var x = 42 ← stack UnsafePointer.alloc(n) ← heap
var s = String("hi") ← heap
(String's internal buffer is on the heap)
Manual Allocation Pattern
from memory import UnsafePointer
fn main():
let size = 8
# 1. Allocate memory for 8 integers on the heap
var buffer = UnsafePointer[Int].alloc(size)
# 2. Initialize each slot
for i in range(size):
buffer.init_pointee_copy(i * i) # 0,1,4,9,16,25,36,49
# 3. Use the memory
var total = 0
for i in range(size):
total += buffer[i]
print("Sum of squares:", total) # 140
# 4. Destroy each stored value (runs __del__ for each element)
for i in range(size):
(buffer + i).destroy_pointee()
# 5. Free the memory block
buffer.free()
Writing a Custom Buffer Struct
Wrap raw allocation in a struct that manages its own memory safely. The __del__ method runs automatically when the struct goes out of scope, preventing leaks.
from memory import UnsafePointer
struct IntBuffer:
var data: UnsafePointer[Int]
var size: Int
fn __init__(inout self, size: Int):
self.size = size
self.data = UnsafePointer[Int].alloc(size)
for i in range(size):
self.data.init_pointee_copy(0)
fn __del__(owned self):
for i in range(self.size):
(self.data + i).destroy_pointee()
self.data.free()
fn set(inout self, index: Int, value: Int):
self.data[index] = value
fn get(self, index: Int) -> Int:
return self.data[index]
fn __len__(self) -> Int:
return self.size
fn main():
var buf = IntBuffer(5)
buf.set(0, 10)
buf.set(1, 20)
buf.set(2, 30)
buf.set(3, 40)
buf.set(4, 50)
for i in range(len(buf)):
print(buf.get(i), end=" ") # 10 20 30 40 50
# buf destroyed here — __del__ frees the heap memory
Struct lifetime + heap memory lifecycle:
IntBuffer created → heap alloc(5)
│
▼ (use buf)
│
buf out of scope → __del__ runs
→ destroy each element
→ free() releases heap block
Common Memory Errors (and How Mojo Prevents Them)
Memory Leak
C programmer mistake: int* ptr = malloc(100); // ... use ptr ... // forgot to call free(ptr) → memory stays reserved forever Mojo approach: Wrap in a struct with __del__. When the struct goes out of scope, __del__ calls free() automatically. Impossible to forget.
Use After Free
C programmer mistake:
free(ptr);
printf("%d", *ptr); // reads freed memory → crash or garbage
Mojo approach:
UnsafePointer becomes invalid conceptually after free().
The ownership system prevents using a moved/consumed pointer.
Double Free
C programmer mistake: free(ptr); free(ptr); // freeing the same address twice → crash Mojo approach: The owned parameter + consume operator (^) transfers the pointer. Once consumed, the original variable cannot be used again.
Interfacing with C Libraries
Many C libraries allocate memory and expect you to pass pointers to their functions. Mojo's UnsafePointer is the bridge.
# Conceptual pattern for calling a C function via FFI
from memory import UnsafePointer
fn process_with_c_lib():
var buf = UnsafePointer[Float32].alloc(1024)
for i in range(1024):
buf.init_pointee_copy(Float32(i))
# Imagine: c_process(buf, 1024) runs here
# The C function reads buf's memory directly
for i in range(1024):
(buf + i).destroy_pointee()
buf.free()
Reallocation Pattern
When a buffer needs to grow, allocate a larger block, copy existing data, and free the old block.
from memory import UnsafePointer
fn grow_buffer(old: UnsafePointer[Int], old_size: Int, new_size: Int) -> UnsafePointer[Int]:
var new_buf = UnsafePointer[Int].alloc(new_size)
for i in range(old_size):
new_buf.init_pointee_copy(old[i])
for i in range(old_size, new_size):
new_buf.init_pointee_copy(0)
return new_buf
Grow diagram:
old: [ 1 | 2 | 3 ] (size 3)
↓ copy
new: [ 1 | 2 | 3 | 0 | 0 ] (size 5)
Then free old.
Key Takeaways
Manual memory management gives maximum control and performance. Use UnsafePointer.alloc(n) to reserve heap memory, initialize each slot, use it, destroy each element, then free the block. Wrap allocations in structs with __del__ to ensure automatic cleanup when the struct goes out of scope. The ownership system prevents use-after-free and double-free bugs even with manual allocation. Reserve manual memory for cases where Mojo's built-in types cannot meet performance or interoperability requirements.
