# Memory Layout of Maru Heap Objects: A Deep Dive into the GC Header Structure

> Explore the Maru heap object memory layout. Understand the GC header structure and how it manages size, type, and GC state for efficient garbage collection.

- Repository: [Attila Lendvai/maru](https://github.com/attila-lendvai/maru)
- Tags: deep-dive
- Published: 2026-02-25

---

**Maru heap objects are contiguous memory blocks that begin with a fixed-size garbage collection header followed by a payload of machine-word slots, where the header tracks size, type, and GC state while the payload holds user data.**

The Maru programming language, maintained in the `attila-lendvai/maru` repository, uses a precise memory layout for heap-allocated objects that enables efficient garbage collection and type-safe access. Understanding this layout is essential for debugging memory issues, extending the runtime, or interfacing with foreign code.

## The GC Header Structure

Every heap object in Maru starts with a **GC header** defined in `source/evaluator/gc.l`. This header occupies a fixed number of bytes immediately preceding the object's payload data.

### Header Field Definitions

The header consists of the following fields as implemented around lines 9–11 and 15–17 of `source/evaluator/gc.l`:

- **`next`** – A pointer linking this chunk to the next entry in the free or used list during mark-and-sweep collection.
- **`size`** – The size of the payload **in bytes**, excluding the header itself.
- **`flags`** – A bit-field containing boolean states: `used` (whether the chunk is allocated), `opaque` (whether the GC should scan the payload), and `mark` (for the collector's mark phase).
- **`type`** – An integer ID representing the object's type, set during allocation to enable dynamic dispatch and type checking.
- **`canary`** – An optional sentinel value (`0xDEADBEE`) present when the `object-canary` feature is enabled, used to detect memory corruption and buffer overruns.

The constant defining the header size is located at `source/evaluator/gc.l#L13`.

## Header vs. Payload Memory Organization

Maru distinguishes between the **header pointer** (used internally by the GC) and the **object pointer** (exposed to user code). When you allocate an object, the runtime returns a pointer to the payload, not the header.

The memory layout follows this structure:

```

+-------------------+    <-- header pointer (internal GC use)
| next  (word)      |
| size  (word)      |
| flags (word)      |
| type  (word)      |
| canary (word)?    |
+-------------------+    <-- object pointer (returned to user code)
| payload slot 0    |    <-- oop-at ptr 0
| payload slot 1    |
| ...               |
| payload slot N-1  |
+-------------------+

```

The helper functions `gc/header-of` and `gc/payload-of` in `source/evaluator/gc.l` (lines 22–26) perform the pointer arithmetic to translate between these addresses. `gc/header-of` subtracts the header size from the object pointer, while `gc/payload-of` adds the header size to the header pointer.

## Allocating and Inspecting Heap Objects

Object allocation occurs in `gc/allocate` around lines 55–64 of `source/evaluator/gc.l`. This function initializes the header fields, clears the payload to zero, and returns the payload pointer.

Maru supports two payload configurations based on the type definition:

- **Boxed one-word objects** – The payload contains exactly one machine word holding the immediate value (used for boxed integers).
- **Boxed multi-word objects** – The payload contains multiple slots as declared by the type's `slot-count-of-instances`.

### Practical Code Examples

```lisp
;; Allocate a boxed single-word integer
(define my-int (box <long> 42))
;; my-int points to the payload; header resides just before it

;; Inspect header metadata
(gc/header-of my-int)                    ; returns header address
(<header>-type (gc/header-of my-int))     ; => type ID of <long>
(<header>-size (gc/header-of my-int))     ; payload size in bytes

;; Allocate a multi-slot object (a pair has two slots)
(define my-pair (box <pair> (list 1 2)))
(oop-at my-pair 0)   ; => first slot value (1)
(oop-at my-pair 1)   ; => second slot value (2)

```

## Navigating the Heap

The garbage collector and debugging tools traverse the heap using linked list pointers stored in the header's `next` field. The functions `gc/first-object` and `gc/next-object` allow iteration over all allocated objects:

```lisp
;; Walk the entire heap and print object metadata
(let ((obj (gc/first-object)))
  (while obj
    (printf "Object at %p, type %ld, size %ld bytes\n"
            obj
            (<header>-type (gc/header-of obj))
            (gc/object-size obj))
    (set obj (gc/next-object obj))))

```

This iteration pattern is used during the mark-and-sweep phases of collection to identify live objects and reclaim unreachable memory.

## Summary

- Maru heap objects consist of a **fixed-size GC header** immediately followed by a variable-length **payload** containing the object's data slots.
- The header tracks **size** (payload bytes), **type** (integer ID), **flags** (used/mark/opaque), and a **next** pointer for heap traversal.
- User code receives pointers to the **payload**, while the GC uses **header pointers**; `gc/header-of` and `gc/payload-of` translate between them.
- **Boxed objects** vary in payload size: single-word payloads for immediate values, multi-word payloads for compound structures like pairs and arrays.
- The optional **canary** field provides memory corruption detection when the `object-canary` feature is enabled.

## Frequently Asked Questions

### What is the size field in the Maru GC header?

The `size` field in the GC header stores the **payload size in bytes**, excluding the header itself. This allows the garbage collector to calculate the total memory footprint by adding the constant header size to this value, and enables proper pointer arithmetic when iterating to the next object in the heap.

### How does Maru distinguish between immediate and boxed objects?

Maru uses the **type system** and allocation strategy rather than header bits to distinguish immediates from boxed objects. Immediate values (like small integers) are encoded directly into pointers or registers when possible, while **boxed objects** are heap-allocated with the standard GC header. The `one-word?` flag in type definitions determines whether a boxed object has a single-word payload or multiple slots.

### Where is the object type information stored in Maru heap objects?

The **type** field in the GC header stores an integer ID representing the object's type. This field is set during allocation in `gc/allocate` and is used by the runtime for dynamic dispatch, type checking, and determining how many slots the payload contains (via `type/slot-count-of-instances`).

### What is the purpose of the canary field in Maru heap objects?

The **canary** field is an optional sentinel value (`0xDEADBEE`) that appears in the GC header when Maru is compiled with the `object-canary` feature enabled. It serves as a **memory corruption detector**—the runtime checks this value during collection and object access to detect buffer overruns, use-after-free errors, or other heap corruption issues.