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.

Leave a Comment

Your email address will not be published. Required fields are marked *