Skip to content

Memory Management

Lee Yunjin edited this page Oct 7, 2026 · 1 revision

Memory Management

Headers: <cwist/core/mem/alloc.h>, <cwist/core/mem/arena.h>, <cwist/core/mem/gc.h>, <cwist/core/mem/intercept.h>

CWIST's default model is explicit: memory you allocate with cwist_alloc() you free with cwist_free(), objects you _create you _destroy. On top of that it offers scoped cleanup, request arenas, and an opt-in epoch-based garbage collector.

The allocator

Function Description
void *cwist_alloc(size_t size) Zeroed memory tracked by libttak.
void *cwist_alloc_array(size_t count, size_t elem_size) Zeroed array (overflow-checked).
void *cwist_realloc(void *ptr, size_t new_size) Resize.
char *cwist_strdup(const char *s) / cwist_strndup(s, n) Copy strings.
void cwist_free(void *ptr) Free memory from any of the above (NULL is fine).
void *cwist_malloc(size_t size) Zeroed memory through the shared owner context (cwist_create_owner()).

Buffers that CWIST functions hand you (serialized responses, URL strings, cookie encodings, JWTs, ...) come from this allocator unless a page says otherwise; release them with cwist_free(). A few APIs use plain malloc instead (db_crypt, db_sync, the metrics renderer, JSON-heal callbacks); their pages say so.

cJSON's allocator is redirected to cwist_alloc at process start, so cJSON memory is released with cJSON_free() / cJSON_Delete() as usual.

Scoped cleanup

On GCC and Clang, attach CWIST_DEFER_FREE to a pointer declaration and the pointer is freed when the enclosing block exits, on every path:

char *token CWIST_DEFER_FREE = cwist_jwt_sign(payload, secret, 3600);
if (!token) return;            /* freed (NULL) here */
send_token(token);             /* and freed at the end of the block */

cwist_alloc_scoped(var, type, size) declares and allocates in one line:

cwist_alloc_scoped(name, char *, 64);
snprintf(name, 64, "user-%d", id);

cwist_scratch_t with CWIST_SCRATCH_DEFER and cwist_scratch_alloc() gives block-scoped scratch memory that prefers a recycled arena generation over the allocator:

cwist_scratch_t s CWIST_SCRATCH_DEFER = {0};
unsigned char *buf = cwist_scratch_alloc(&s, needed);   /* not zeroed */

Only use these for pointers that do not leave the block (not stored in a struct, a global or a queued job). On compilers without the cleanup attribute (TinyCC) they do nothing and you must free explicitly.

Request arenas

<cwist/core/mem/arena.h> provides bump allocation from a libttak arena generation, released in one step. CWIST allocates each request's and response's fixed structures this way (a full HTTP/1.1 exchange uses about 3.5 KB of the 8 KiB default generation; spills go to the heap).

Function Description
cwist_arena_t *cwist_arena_create(size_t generation_bytes) Arena (0 = 8 KiB default, recycled per thread). NULL on failure.
void *cwist_arena_alloc(cwist_arena_t *a, size_t size) Aligned, not zeroed; NULL when the generation is full.
bool cwist_arena_owns(const cwist_arena_t *a, const void *p) Whether p came from the arena.
void cwist_arena_destroy(cwist_arena_t *a) Release everything at once.

Full GC (opt-in)

cwist_full_gc(true) turns on epoch-based reclamation for the whole process: cwist_alloc blocks a thread forgets to free are reclaimed, and connections a thread or the process still owns at exit are closed. Pointers that must outlive their scope are handed off with cwist_gc_scope_disown(). Opting a translation unit into CWIST_INTERCEPT_MALLOC routes its plain malloc/free through the same pipeline. Read Full GC for the design and measured costs, and Async GC Ownership for handing memory between threads.

libttak

CWIST links libttak for its allocator and concurrency primitives: generational arenas (static files and cache blobs are retired one generation at a time), epoch-based reclamation (readers never block while buffers are swapped), detachable memory for zero-copy bodies, and a lock-free job queue. CWIST_MALLOC_ARENA_MAX=1 caps glibc's malloc arenas per worker; see Configuration and Environment.

Clone this wiki locally