| name | memory |
| description | talloc-based memory management and ownership rules for ikigai |
Memory Management
talloc hierarchical memory allocator for ikigai. Use this for all new code.
Why talloc?
- Automatic cleanup - Parent context frees all children
- Ownership hierarchy - Natural mapping to object lifecycles
- Debugging built-in - Leak detection and memory tree reporting
- Battle-tested - Used in Samba, proven reliable
Ownership Rules
- Caller owns returned pointers - Functions transfer ownership
- Each allocation has one owner - That owner frees it
- Children freed with parent - talloc hierarchy does this automatically
- Document ownership - Make it explicit in function comments
Zero-Initialization Rule
Always use zero-initialized allocations (talloc_zero, talloc_zero_array) unless you intentionally choose not to for performance reasons. Uninitialized memory is a source of bugs.
foo_t *foo = talloc_zero(ctx, foo_t);
int *arr = talloc_zero_array(ctx, int, 100);
foo_t *foo = talloc(ctx, foo_t);
Core API
TALLOC_CTX *talloc_new(const void *parent);
void *talloc(const void *ctx, type);
void *talloc_zero(const void *ctx, type);
void *talloc_array(const void *ctx, type, count);
char *talloc_strdup(const void *ctx, const char *str);
char *talloc_asprintf(const void *ctx, const char *fmt, ...);
int talloc_free(void *ptr);
void *talloc_steal(const void *new_parent, const void *ptr);
void *talloc_reference(const void *ctx, const void *ptr);
Pattern 1: Short-lived Request Processing
void handle_request(const char *input) {
TALLOC_CTX *req_ctx = talloc_new(NULL);
res_t res = ik_protocol_msg_parse(req_ctx, input);
if (is_err(&res)) {
talloc_free(req_ctx);
return;
}
ik_protocol_msg_t *msg = res.ok;
talloc_free(req_ctx);
}
Pattern 2: Allocate on Caller's Context
res_t ik_cfg_load(TALLOC_CTX *ctx, const char *path) {
ik_cfg_t *config = talloc_zero_(ctx, sizeof(ik_cfg_t));
if (!config) PANIC("Out of memory");
config->openai_api_key = talloc_strdup(config, key_from_file);
config->listen_address = talloc_strdup(config, addr_from_file);
return OK(config);
}
TALLOC_CTX *ctx = talloc_new(NULL);
res_t res = ik_cfg_load(ctx, "config.json");
ik_cfg_t *config = res.ok;
talloc_free(ctx);
Pattern 3: Struct Fields as Children
Correct - Fields are children of struct:
res_t foo_init(TALLOC_CTX *ctx, foo_t **out) {
foo_t *foo = talloc_zero_(ctx, sizeof(foo_t));
foo->name = talloc_strdup(foo, "example");
foo->data = talloc_array(foo, char, 1024);
*out = foo;
return OK(*out);
}
Wrong - Fields as siblings:
foo_t *foo = talloc_zero_(ctx, sizeof(foo_t));
foo->name = talloc_strdup(ctx, "example");
Pattern 4: Temporary Contexts
res_t process(TALLOC_CTX *ctx, input_t *in) {
TALLOC_CTX *tmp = talloc_new(ctx);
char *buf = talloc_array(tmp, char, 4096);
parsed_t *p = talloc_zero(tmp, parsed_t);
result_t *result = process_internal(tmp, in);
if (keep_result) {
talloc_steal(ctx, result);
}
talloc_free(tmp);
return OK(result);
}
Avoiding Fixed-Size Allocations
CRITICAL: Never use fixed-size allocations unless you know the exact size of the data.
Strategies for unknown sizes:
- Determine size first: stat() for files, Content-Length for HTTP
- Growable buffers: Start small, use talloc_realloc() to grow
- Read-measure-reread: Count bytes first pass, allocate exact size, reread
- Return errors: Fail if data exceeds reasonable limit
Common cases:
- String building →
talloc_asprintf(ctx, fmt, ...)
- Path building →
talloc_asprintf(ctx, "%s/%s", dir, file)
- File reading →
stat() first, allocate st.st_size, then read()
Rule: If you don't know the size, use dynamic allocation.
CRITICAL: Error Context Lifetime
DANGER: Never allocate errors on temporary contexts.
res_t bad_example(TALLOC_CTX *ctx) {
TALLOC_CTX *tmp = talloc_new(ctx);
res_t res = some_function(tmp);
if (is_err(&res)) {
talloc_free(tmp);
return res;
}
talloc_free(tmp);
return OK(NULL);
}
res_t good_example(TALLOC_CTX *ctx) {
TALLOC_CTX *tmp = talloc_new(ctx);
res_t res = some_function(ctx);
if (is_err(&res)) {
talloc_free(tmp);
return res;
}
talloc_free(tmp);
return OK(NULL);
}
Rule: Functions that can fail should allocate errors on the parent context (usually first parameter), not on temporary contexts.
Function Naming Conventions
*_init(TALLOC_CTX *ctx, foo_t **out) - Allocate on ctx, return via out parameter
*_create() - Allocates and returns owned pointer
*_load() - Allocates and returns owned pointer (from file/network)
*_free() - Deallocates object and all children
*_parse(TALLOC_CTX *ctx, ...) - Parse and allocate on ctx
When NOT to Use talloc
Rare cases for plain malloc():
- FFI boundaries - Libraries expecting
free()-able memory
- Long-lived singletons - Global state for entire program
Default: Use talloc for everything else.
OOM Handling
Memory allocation failures call PANIC("Out of memory") which terminates the process immediately. OOM is not a recoverable error.
void *ptr = talloc_zero(ctx, type);
if (!ptr) PANIC("Out of memory");
Debugging
talloc_enable_leak_report();
talloc_enable_leak_report_full();
talloc_report_full(context, stdout);
atexit(talloc_report_full_on_exit);
Common Mistakes
- Allocating fields on wrong parent - Use
talloc_*(struct, ...) not talloc_*(ctx, ...)
- Freeing struct but not fields - Make fields children, not siblings
- Error on temp context - Pass parent context to functions that can fail
- talloc_new(NULL) outside main() - Should receive parent from caller
- Mixing malloc/free with talloc - Pick one, use talloc
Quick Reference
Create context:
TALLOC_CTX *ctx = talloc_new(parent);
Allocate:
foo_t *foo = talloc_zero_(ctx, sizeof(foo_t));
char *str = talloc_strdup(ctx, "text");
int *arr = talloc_array(ctx, int, 100);
Free:
talloc_free(ctx);
Move ownership:
talloc_steal(new_parent, ptr);
For full details, see project/memory.md.