memory.h — Reference-Counted Memory Management

Introduction

memory.h provides a vtable-driven, reference-counted memory management system for C. It enables object lifecycle management (construction, destruction, retain, release, copy, move) through a virtual table pattern, bringing RAII-like semantics to pure C. The XMALLOC(T) macro allocates an object with an embedded header that tracks the reference count and vtable pointer.

Design Philosophy

  1. vtable-Driven Lifecycle — Each object type defines a static xVTable with optional function pointers for ctor, dtor, retain, release, copy, and move. This decouples lifecycle logic from the allocation mechanism, similar to C++ virtual destructors or Objective-C's class methods.

  2. Hidden Header Pattern — A Header struct is prepended to every allocation, storing the type name (for debugging), size, reference count, and vtable pointer. The user receives a pointer past the header, so the header is invisible to normal usage.

  3. Atomic Reference CountingxRetain() and xRelease() use atomic operations (__ATOMIC_SEQ_CST) to safely manage reference counts across threads. When the count reaches zero, the destructor is called and memory is freed.

  4. Macro ConvenienceXMALLOC(T) and XMALLOCEX(T, sz) macros generate the correct xAlloc() call with the type name string, size, and vtable pointer, reducing boilerplate.

Architecture

graph TD
    MACRO["XMALLOC(T) / XMALLOCEX(T, sz)"]
    ALLOC["xAlloc(name, size, count, vtab)"]
    HEADER["Header + Object"]
    RETAIN["xRetain(ptr)<br/>atomic refs++"]
    RELEASE["xRelease(ptr)<br/>atomic refs--"]
    FREE["xFree(ptr)<br/>dtor + free"]
    COPY["xCopy(ptr, other)"]
    MOVE["xMove(ptr, other)"]

    MACRO --> ALLOC
    ALLOC --> HEADER
    HEADER --> RETAIN
    HEADER --> RELEASE
    RELEASE -->|"refs == 0"| FREE
    HEADER --> COPY
    HEADER --> MOVE

    style MACRO fill:#4a90d9,color:#fff
    style RELEASE fill:#e74c3c,color:#fff
    style FREE fill:#e74c3c,color:#fff

Implementation Details

Memory Layout

graph LR
    subgraph "malloc'd block"
        HDR["Header<br/>name | size | refs | vtab"]
        OBJ["User Object<br/>(sizeof(T) bytes)"]
        EXTRA["Extra bytes<br/>(XMALLOCEX only)"]
    end

    PTR["xAlloc() returns →"] --> OBJ

    style HDR fill:#f5a623,color:#fff
    style OBJ fill:#4a90d9,color:#fff
    style EXTRA fill:#50b86c,color:#fff

The actual memory layout:

┌──────────────────────────────────────────────────────┐
│ Header (hidden)                                      │
│   const char *name   — type name string (e.g. "Foo") │
│   size_t      size   — sizeof(T)                     │
│   size_t      refs   — reference count (starts at 1) │
│   xVTable    *vtab   — pointer to static vtable      │
├──────────────────────────────────────────────────────┤
│ User Object (returned pointer)                       │
│   T fields...                                        │
│   [optional extra bytes from XMALLOCEX]              │
└──────────────────────────────────────────────────────┘

XMALLOC / XMALLOCEX Macro Expansion

// Given:
typedef struct Foo Foo;
struct Foo { int x; char buf[]; };

XDEF_VTABLE(Foo) { .ctor = FooCtor, .dtor = FooDtor };
XDEF_CTOR(Foo) { self->x = 0; }
XDEF_DTOR(Foo) { /* cleanup */ }

// XMALLOC(Foo) expands to:
(Foo *)xAlloc("Foo", sizeof(Foo), 1, &FooVTable)

// XMALLOCEX(Foo, 128) expands to:
(Foo *)xAlloc("Foo", sizeof(Foo) + 128, 1, &FooVTable)

Reference Count Lifecycle

sequenceDiagram
    participant App
    participant Alloc as xAlloc
    participant Header
    participant VTable

    App->>Alloc: XMALLOC(Foo)
    Alloc->>Header: malloc(sizeof(Header) + sizeof(Foo))
    Alloc->>Header: refs = 1
    Alloc->>VTable: vtab->ctor(ptr)
    Alloc-->>App: Foo *ptr

    App->>Header: xRetain(ptr) → refs = 2
    App->>Header: xRelease(ptr) → refs = 1
    App->>Header: xRelease(ptr) → refs = 0
    Header->>VTable: vtab->release(ptr)
    Header->>VTable: vtab->dtor(ptr)
    Header->>Header: free(hdr)

Thread Safety

  • xRetain() and xRelease() are thread-safe — they use xAtomicAdd / xAtomicSub with sequential consistency ordering.
  • xAlloc(), xFree(), xCopy(), and xMove() are not thread-safe — they should be called from a single owner or with external synchronization.

API Reference

Macros

MacroExpansionDescription
XDEF_VTABLE(T)static xVTable TVTable =Define a static vtable for type T
XDEF_CTOR(T)static void TCtor(T *self)Define a constructor for type T
XDEF_DTOR(T)static void TDtor(T *self)Define a destructor for type T
XMALLOC(T)(T *)xAlloc("T", sizeof(T), 1, &TVTable)Allocate one T with vtable
XMALLOCEX(T, sz)(T *)xAlloc("T", sizeof(T) + sz, 1, &TVTable)Allocate T + extra bytes

Types

TypeDescription
xVTableStruct with function pointers: ctor, dtor, retain, release, copy, move

Functions

FunctionSignatureDescriptionThread Safety
xAllocvoid *xAlloc(const char *name, size_t size, size_t count, xVTable *vtab)Allocate object(s) with header and call ctor.Not thread-safe
xFreevoid xFree(void *ptr)Call dtor and free. Ignores NULL.Not thread-safe
xRetainvoid xRetain(void *ptr)Increment reference count atomically. Calls vtab->retain if set.Thread-safe
xReleasevoid xRelease(void *ptr)Decrement reference count atomically. Calls vtab->release then xFree when refs reach 0.Thread-safe
xCopyvoid xCopy(void *ptr, void *other)Call vtab->copy if set.Not thread-safe
xMovevoid xMove(void *ptr, void *other)Call vtab->move if set.Not thread-safe

Usage Examples

Basic Object with Constructor/Destructor

#include <stdio.h>
#include <string.h>
#include <xbase/memory.h>

typedef struct Connection Connection;
struct Connection {
    int fd;
    char host[256];
};

XDEF_CTOR(Connection) {
    self->fd = -1;
    memset(self->host, 0, sizeof(self->host));
    printf("Connection created\n");
}

XDEF_DTOR(Connection) {
    if (self->fd >= 0) {
        // close(self->fd);
        printf("Connection closed (fd=%d)\n", self->fd);
    }
}

XDEF_VTABLE(Connection) {
    .ctor = ConnectionCtor,
    .dtor = ConnectionDtor,
};

int main(void) {
    Connection *conn = XMALLOC(Connection);
    conn->fd = 42;
    strcpy(conn->host, "example.com");

    xRetain(conn);   // refs = 2
    xRelease(conn);  // refs = 1
    xRelease(conn);  // refs = 0 → dtor called → freed

    return 0;
}

Flexible Array Member with XMALLOCEX

#include <stdio.h>
#include <string.h>
#include <xbase/memory.h>

typedef struct Buffer Buffer;
struct Buffer {
    size_t len;
    char   data[];  // flexible array member
};

XDEF_CTOR(Buffer) { self->len = 0; }
XDEF_DTOR(Buffer) { /* nothing to clean up */ }
XDEF_VTABLE(Buffer) { .ctor = BufferCtor, .dtor = BufferDtor };

int main(void) {
    // Allocate Buffer + 1024 extra bytes for data[]
    Buffer *buf = XMALLOCEX(Buffer, 1024);

    memcpy(buf->data, "Hello, xKit!", 12);
    buf->len = 12;

    printf("Buffer: %.*s\n", (int)buf->len, buf->data);

    xRelease(buf); // refs 1 → 0 → freed
    return 0;
}

Use Cases

  1. Shared Ownership — Multiple components hold references to the same object (e.g., a connection shared between a reader and a writer). xRetain/xRelease ensures the object is freed only when the last reference is dropped.

  2. Plugin/Extension Objects — Define vtables for different object types that share a common interface. The vtable pattern enables polymorphic behavior in C.

  3. Debug-Friendly Allocation — The name field in the header enables allocation tracking and leak detection by type name.

Best Practices

  • Always pair xRetain with xRelease. Every retain must have a corresponding release, or you'll leak memory.
  • Use XMALLOC instead of raw xAlloc. The macro handles type name, size, and vtable automatically.
  • Set unused vtable fields to NULL. The implementation checks for NULL before calling each vtable function.
  • Don't mix with free(). Objects allocated with xAlloc have a hidden header. Calling free() directly on the user pointer corrupts the heap.
  • Use XMALLOCEX for flexible array members. It adds extra bytes after the struct for variable-length data.

Comparison with Other Libraries

Featurexbase memory.hC++ RAIIObjective-C ARCGLib GObject
Mechanismvtable + atomic refcountDestructor + smart pointersCompiler-inserted retain/releaseGType + refcount
AutomationManual retain/releaseAutomatic (scope-based)Automatic (compiler)Manual ref/unref
Thread SafetyAtomic refcountshared_ptr is atomicAtomicAtomic
Polymorphismvtable function pointersVirtual functionsMethod dispatchSignal/slot + vtable
Overhead1 header per object (~32 bytes)0 (stack) or control block1 isa pointer + refcountLarge (GTypeInstance)
Flexible ArraysXMALLOCEX(T, sz)std::vectorNSMutableDataGArray
Debug InfoType name in headerRTTIClass nameGType name
LanguageC99C++Objective-CC (with macros)

Key Differentiator: xbase's memory system brings reference-counted lifecycle management to C with minimal overhead — just a 32-byte header per object. The vtable pattern provides extensibility (custom ctor/dtor/copy/move) without requiring a complex type system like GObject.

Benchmark

Environment: Apple M3 Pro, 36 GB RAM, macOS 26.4, Release build (-O2). Source: xbase/memory_bench.cpp

BenchmarkSize (bytes)Time (ns)CPU (ns)Iterations
BM_Memory_XAlloc1623.323.329,809,940
BM_Memory_XAlloc6421.121.132,551,024
BM_Memory_XAlloc25622.422.431,207,508
BM_Memory_XAlloc1,02420.120.134,024,352
BM_Memory_XAlloc4,09624.224.229,002,681
BM_Memory_Malloc1617.517.539,883,995
BM_Memory_Malloc6418.718.737,576,831
BM_Memory_Malloc25619.019.034,505,536
BM_Memory_Malloc1,02423.023.030,557,144
BM_Memory_Malloc4,09617.717.739,849,483
BM_Memory_RetainRelease3.903.90183,068,277

Key Observations:

  • xAlloc vs malloc overhead is only ~3–5ns across all sizes. The extra cost covers header initialization, vtable setup, and constructor invocation — negligible for most workloads.
  • Retain/Release cycle takes ~3.9ns, dominated by the atomic increment/decrement. This is fast enough for hot-path reference counting.
  • Allocation time is nearly constant across sizes (16B–4KB), confirming that the overhead is in the header management, not the underlying malloc.