playbit / docs

Memory

Memory management in the Playbit C is done with explicit allocators. The type of an allocator is PBMem and is passed as an argument to functions which allocate memory. Every distinct allocation from PBMem allocators have an address-aligment guarantee of PBMem_ALIGNMENT (at least 16 bytes).

Example

#include <playbit/playbit.h>

int* make_int_array_or_panic(PBMem ma, usize count) {
    int* array = PBMemAllocz(ma, count * sizeof(int));
    return PBExpectNotNull(array); // panic if allocation failed
}

void main() {
    PBMem ma = kPBMemGPA;
    int* a = make_int_array(ma, 12);
    print("allocated array of 12 integers at %p", a);
    PBMemFree(ma, a);
}

Allocators

There are several allocators available "out of the box." Usually you just use kPBMemGPA and that's it, but sometimes a specialized allocator can be useful. For example, if you are parsing an AST, analyzing it and then throwing away the result, PBArena removes the need for freeing memory. A PBMemNullAllocator is useful when testing code that gracefully handles allocation failures.


#include <playbit/mem.h>

PBMem_ALIGNMENT

#define PBMem_ALIGNMENT usize

PBMem_ALIGNMENT is the minimum guaranteed address alignment for allocations. It's at least _Alignof(max_align_t)

PBMem

typedef const struct PBMemAllocator * PBMem;

PBMem is the type of a memory allocator

PBMemAllocator

typedef struct PBMemAllocator {
    void* nullable (*f)(PBMem self, void* nullable ptr, usize* nullable nbyte_inout, u32 flags);
} PBMemAllocator;

PBMemAllocator is the type of a memory allocator. It's function is invoked as follows:

PBMem libc equivalent
f(NULL, size, 0) malloc(size)
f(NULL, size, PBMem_ZERO) calloc(size, 1)
f(ptr, size, 0) realloc(ptr, size)
f(ptr, size, PBMem_ZERO) realloc(ptr, size2);
bzero(ptr2+size1, size2-size1)
f(ptr, 0, 0) free(ptr)

It's defined as a struct with a function pointer, rather than simply a function, to allow allocator implementations like arenas to store allocator-specific state.

PBMem_ZERO

const int PBMem_ZERO = 1;

allocate zeroed memory

PBMem_ROUNDUP

const int PBMem_ROUNDUP = 2;

PBMem_ROUNDUP requests that nbyte_inout is rounded up to a value that the allocator can allocate without adding any padding.

PBMemAlloc

void* PBMemAlloc(PBMem ma,
                 usize nbyte);

PBMemAlloc allocates nbyte bytes through allocator ma.

PBMemAllocz

void* PBMemAllocz(PBMem ma,
                  usize nbyte);

PBMemAllocz allocates nbyte bytes through allocator ma and zero-initializes the returned memory.

PBMemRealloc

void* PBMemRealloc(PBMem ma,
                   void* ptr,
                   usize nbyte);

PBMemRealloc resizes an allocation owned by ma.

PBMemReallocz

void* PBMemReallocz(PBMem ma,
                    void* ptr,
                    usize nbyte);

PBMemReallocz is like PBMemRealloc but requests zero-initialization of newly added bytes.

PBMemAllocx

void* PBMemAllocx(PBMem  ma,
                  void*  ptr,
                  usize* nbyte_inout,
                  u32    flags);

PBMemAllocx is the most flexible allocator interface. You can perform all actions provided by PBMemAlloc, PBMemAllocz, PBMemRealloc, PBMemReallocz and PBMemFree, plus get the actual allocated memory region's size back.

PBMemFree

void PBMemFree(PBMem ma,
               void* ptr);

PBMemFree releases ptr to allocator ma.

PBMemResize

bool PBMemResize(PBMem  ma,
                 usize  elemSize,
                 void*  ptrInOut,
                 usize* capInOut,
                 usize  newCap,
                 u32    maFlags);

PBMemResize resizes the capacity of an array *ptrInOut to newCap.

PBMemGrow

bool PBMemGrow(PBMem  ma,
               usize  elemSize,
               void*  ptrInOut,
               usize* capInOut,
               usize  addCap,
               u32    maFlags);

PBMemGrow attempts to grow the array at *dataPtr to at least *capInOut+addCap, rounding up by allocator-friendly amounts. Usually ~1.5x of current capacity.

PBMemPageSize

usize PBMemPageSize();

PBMemPageSize returns the underlying system's memory page size

PBMemEq

bool PBMemEq(const void* a,
             usize       aLen,
             const void* b,
             usize       bLen);

PBMemEq returns true if memory at a is equivalent to memory at b

PBMemCmp

int PBMemCmp(const void* a,
             usize       aLen,
             const void* b,
             usize       bLen);

PBMemCmp returns <0 if a is bytewise less than b, >1 if b < a, 0 if a == b

PBMemoryCopy

void PBMemoryCopy(void*       to,
                  const void* from,
                  usize       size);

PBMemoryCopy is a different name for memcpy

PBMemoryZero

void PBMemoryZero(void* ptr,
                  usize size);

PBMemoryZero does memset(ptr, 0, size)

PBMemoryEquals

bool PBMemoryEquals(const void* a,
                    const void* b,
                    usize       size);

PBMemoryZeroStruct

#define PBMemoryZeroStruct(ptr)

PBMemoryZeroStruct does memset(ptr, 0, sizeof(*ptr))

PBMemRegionsOverlap

bool PBMemRegionsOverlap(const void* a,
                         usize       aLen,
                         const void* b,
                         usize       bLen);

PBMemRegionsOverlap returns true if two memory regions overlap


#include <playbit/mem_gpa.h>

kPBMemGPA

extern PBMem kPBMemGPA;

kPBMemGPA is the general-purpose allocator

PBMemGPAStats

typedef struct PBMemGPAStats {
    usize totalBytes; // size of heap, in bytes
    usize peakBytes;  // max space allocated from system, in bytes
    usize freeBytes;  // free space, in bytes
    usize usedBytes;  // allocated space, in bytes
    usize usedCount;  // number of individual, active allocations
} PBMemGPAStats;

PBMemGPAStats describes info about the kPBMemGPA's state

PBMemGPAGetStats

void PBMemGPAGetStats(PBMem          m,
                      PBMemGPAStats* result);

PBMemGPAGetStats retrieves current stats of a GPA.


#include <playbit/mem_buf_allocator.h>

PBMemBufAllocator

typedef struct PBMemBufAllocator {
    PBMemAllocator allocator;
    u8*            buf;
    usize          offs; // allocation offset, in bytes (i.e. memory allocated, in use)
    usize          cap;  // capacity of buf, in bytes
#if USIZE_MAX >= 0xffffffffffffffff
    usize lastSize : 56; // size in bytes of most recent allocation
    usize flags : 8;
#else
    usize lastSize;
    u8    flags;
#endif
} PBMemBufAllocator;

PBMemBufAllocator_ZEROED

const int PBMemBufAllocator_ZEROED = 1;

initial buf is zeroed

PBMemBufAllocator_THREADSAFE

const int PBMemBufAllocator_THREADSAFE = 2;

enable allocations across threads

PBMemBufAllocatorInit

PBMem PBMemBufAllocatorInit(PBMemBufAllocator* a,
                            void*              buf,
                            usize              bufCap,
                            u8                 flags);

PBMemBufAllocatorInit initializes a bump allocator on top of buf.

If address of buf is not aligned to PBMem_ALIGNMENT, the actual usable size may be smaller than bufCap.

PBMemBufAllocator is not thread safe unless PBMemBufAllocator_THREADSAFE flag is set. If PBMemBufAllocator_THREADSAFE is set, only allocations are supported; resizing (realloc) will fail and free will have no effect.

PBMemBufAllocatorReset

void PBMemBufAllocatorReset(PBMemBufAllocator* a);

PBMemBufAllocatorReset frees any and all allocations made in the allocator

PBMemBufAllocatorScopeBegin

usize PBMemBufAllocatorScopeBegin(PBMemBufAllocator* a);

PBMemBufAllocatorScopeBegin marks the beginning of a resumable state of the allocator. Any allocations made after a call to PBMemBufAllocatorScopeBegin are freed all at once by a matchin call to PBMemBufAllocatorScopeEnd.

PBMemBufAllocatorScopeEnd

void PBMemBufAllocatorScopeEnd(PBMemBufAllocator* a,
                               usize              scope);

PBMemBufAllocatorScopeBegin ends a region started by PBMemBufAllocatorScopeBegin


#include <playbit/mem_null_allocator.h>

PBMemNullAllocator

int PBMemNullAllocator();

PBMemNullAllocator always fails to allocate memory, intended to be used for testing


#include <playbit/mem_zeroing_allocator.h>

PBMemZeroingAllocator

typedef struct PBMemZeroingAllocator {
    PBMemAllocator ma;
    PBMem          source;
} PBMemZeroingAllocator;

PBMemZeroingAllocator wraps another allocator and sets the PBMem_ZERO flag for all allocations

PBMemZeroingAllocatorImpl

void* PBMemZeroingAllocatorImpl(PBMem  m,
                                void*  p,
                                usize* zp,
                                u32    fl);

PBMemZeroingAllocatorImpl forwards allocation to the wrapped source allocator and forces PBMem_ZERO.

PBMemZeroingAllocatorInit

PBMem PBMemZeroingAllocatorInit(PBMemZeroingAllocator* a,
                                PBMem                  source);

PBMemZeroingAllocatorInit initializes a PBMemZeroingAllocator wrapper and returns it as PBMem.

All allocations and reallocations performed through the returned allocator force PBMem_ZERO, regardless of caller flags.

PBMemZeroingAllocatorWrap

macro PBMem PBMemZeroingAllocatorWrap(PBMem sourceAllocator)

#include <playbit/arena.h>

PBArena is an arena allocator, useful for region-based memory management. It's a hybrid betwen a simple bump allocator and a chained-block allocator.

Example of basic use:

// create an arena that sources memory blocks from the GPA
PBArena* arena = PBArenaAlloc(.allocator = kPBMemGPA);
int* array1 = PBArenaPush(arena, 4 * sizeof(int), alignof(int), 0);
// free last 4*4 bytes allocated
void PBArenaPop(arena, 4 * sizeof(int));
int* array2 = PBArenaPush(arena, 4 * sizeof(int), alignof(int), 0);
int* array3 = PBArenaPush(arena, 4 * sizeof(int), alignof(int), 0);
// free all memory allocated:
PBArenaReset(arena);
// free the arena itself
PBArenaFree(&arena);

Example use as a PBMem allocator:

PBArena* arena = PBArenaAlloc(.allocator = kPBMemGPA);
PBMem ma = PBMemFromArena(arena);
SomeFunctionThatUsesPBMem(ma);
// free all memory allocated with `ma`
PBArenaFree(&arena);

Example of scoped allocations:

PBArena* arena = PBArenaAlloc(.allocator = kPBMemGPA);
u8* a = PBArenaPush(arena, 128, 1, 0);
{
    PBArenaScope(arena);
    u8* b = PBArenaPush(arena, 128, 1, 0);
    expect(b == a);
    // all allocations made in this scope are freed when leaving
}
{
    PBArenaScope(arena);
    u8* b = PBArenaPush(arena, 128, 1, 0);
    expect(b == a);
}
PBArenaFree(&arena);

PBArenaFlags

typedef enum PBArenaFlags PB_ENUM_TYPE(u64){
    PBArenaFlag_NoChain = (1 << 0),
    PBArenaFlag_FixedBuffer = (1 << 1),
} PBArenaFlags;

PBArena

struct PBArena {
    // PBMem
    PBMemAllocator ma;

    // config
    PBMem        allocator;
    PBArenaFlags flags;

    // chains
    PBArena*          current;
    PBArena* nullable prev;
    usize             basePos;

    // arena state
    u8*   data;
    usize pos;
    usize size;

    // debug
    char* debugFile;
    int   debugLine;
};

PBArenaParams

struct PBArenaParams {
    // config
    PBMem        allocator;
    PBArenaFlags flags;
    usize        initialSize;
    void*        backingBuffer; // you must set initialSize if using!

    // debug
    char* debugFile;
    int   debugLine;
};

PBArenaTemp

struct PBArenaTemp {
    PBArena* arena;
    usize    pos;
};

PBArena_HEADER_SIZE

#define PBArena_HEADER_SIZE

PBArena_HEADER_SIZE is the number of bytes of overhead per arena slab

PBArenaAllocP

PBArena* PBArenaAllocP(PBArenaParams params);

PBArenaAllocP creates an arena from params.

If params.backingBuffer is set, that memory is used as fixed backing storage. Otherwise memory is allocated from params.allocator.

PBArenaAlloc

macro PBArena* nullable PBArenaAlloc(.field = value ...)

creates an arena from params, a convenience wrapper for PBArenaAllocP((PBArenaParams){ .field = value ... })

PBArenaFree

void PBArenaFree(PBArena* arena);

PBArenaFree releases all memory owned by *arena and sets *arena to NULL.

PBArenaFromBuffer

PBArena* PBArenaFromBuffer(void* data,
                           usize size);

PBArenaFromBuffer creates an arena backed by caller-owned memory in [data, data+size).

PBArenaFromAllocator

PBArena* PBArenaFromAllocator(PBMem allocator,
                              usize size);

PBArenaFromAllocator creates an arena that allocates slab memory from allocator.

PBArenaPush

void* PBArenaPush(PBArena* arena,
                  usize    size,
                  usize    align,
                  bool     zero);

PBArenaPush allocates size bytes from arena with alignment align.

If zero is true, returned bytes are zero-initialized.

PBArenaPop

void PBArenaPop(PBArena* arena,
                usize    size);

PBArenaPop releases the most-recent size bytes from arena state.

PBArenaPopTo

void PBArenaPopTo(PBArena* arena,
                  usize    pos);

PBArenaPopTo rewinds arena allocation position to absolute offset pos.

PBArenaReset

void PBArenaReset(PBArena* arena);

PBArenaReset rewinds arena to position 0, releasing all allocations.

PBArenaPos

usize PBArenaPos(const PBArena* arena);

PBArenaPos returns the current absolute allocation position.

PBArenaSize

usize PBArenaSize(const PBArena* arena);

PBArenaSize returns total bytes across all slabs currently attached to arena.

PBArenaTempBegin

PBArenaTemp PBArenaTempBegin(PBArena* arena);

PBArenaTempBegin snapshots current arena position for scoped rollback.

PBArenaTempEnd

void PBArenaTempEnd(PBArenaTemp temp);

PBArenaTempEnd rewinds arena to the position captured by temp.

PBMemFromArena

PBMem PBMemFromArena(PBArena* arena);

PBMemFromArena returns an allocator that can be used with anything that accepts PBMem

PBArenaIsEmpty

bool PBArenaIsEmpty(const PBArena* arena);

PBArenaIsEmpty reports whether arena has no usable backing storage. This is true for an arena initialized from a zero-sized buffer or after PBArenaFree has invalidated its slabs.

PBArenaScope

void PBArenaScope(PBArena* arena);

PBArenaScope brackets the current scope in PBArenaTempBegin and PBArenaTempEnd calls, effectively freeing any memory allocated in this arena during the span of the current scope.