Last active
January 14, 2025 23:27
-
-
Save saolsen/f47fadfe68484104539c7a4c9507bd21 to your computer and use it in GitHub Desktop.
Single header c library
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Single header file library for C. | |
// * Provides an arena allocator and some data structures that use it. | |
// | |
// * Maintains a (thread local) pool of arenas so they are cheap to acquire and release for temporary allocations. | |
// Since all allocations are backed by arenas you don't have to free anything individually, just | |
// use an arena that matches the lifetime of the data. | |
// * Includes data structures like a dynamic array that work well with the arena. | |
// * Allows for some optimizations like extending instead of reallocing if the array is on the end of an arena. | |
// * All lengths are signed. | |
// * Makes for less casting when comparing lengths and pointers. | |
// * If compiling with -Wconversion you probably have to cast to size_t to call stdlib stuff. | |
// * Rules of thumb. | |
// * If a function returns something that needs to be allocated, it should take an arena to allocate the result on. | |
// * You should never return something allocated on an arena that wasn't passed in. | |
// * If a function needs to allocate temporary memory, | |
// it should acquire its own arena and release it before returning. | |
// * Things to add. | |
// * HashMap. | |
// * Pool | |
// * Handles | |
#ifndef STEVE_H | |
#define STEVE_H | |
#include <stdint.h> | |
#include <stddef.h> | |
#define MIN(x, y) ((x) <= (y) ? (x) : (y)) | |
#define MAX(x, y) ((x) >= (y) ? (x) : (y)) | |
#define CLAMP_MAX(x, max) MIN(x, max) | |
#define CLAMP_MIN(x, min) MAX(x, min) | |
#define IS_POW2(x) (((x) != 0) && ((x) & ((x)-1)) == 0) | |
#define ALIGN_DOWN(n, a) ((n) & ~((a) - 1)) | |
#define ALIGN_UP(n, a) ALIGN_DOWN((n) + (a) - 1, (a)) | |
#define ALIGN_DOWN_PTR(p, a) ((void *)ALIGN_DOWN((ptrdiff_t)(p), (a))) | |
#define ALIGN_UP_PTR(p, a) ((void *)ALIGN_UP((ptrdiff_t)(p), (a))) | |
uint64_t pow2_next(uint64_t i) { | |
if (i == 0) return 0; | |
i--; | |
i |= i >> 1; | |
i |= i >> 2; | |
i |= i >> 4; | |
i |= i >> 8; | |
i |= i >> 16; | |
i |= i >> 32; | |
i++; | |
return i; | |
} | |
// memcpy wrapper so we don't have to include sting.h in the header. | |
// @todo(steve): Tie in with future error handling. | |
void* xmemcpy(void *dest, const void *src, ptrdiff_t n); | |
// Arena | |
// * An arena is a continuous block of memory that can grow up to a certain size. | |
// * They currently have a max capacity of 1024*1024 pages. On my m1 mac that's 64GB. | |
// This is probably too big. @todo(steve): Make this configurable. | |
// * There is a thread_local pool of arenas. | |
// when they are released their memory is not returned to the operating system, | |
// but they are reset and available for use again. | |
// * This makes arena_acquire a cheap operation so they can be used both for long-lived things | |
// and for temporary allocations within a function. | |
// * Arenas are not thread safe so multiple threads should not use the same arena at the same time. | |
// * This only works on 64 bit systems where pointers are 64 bits. | |
static_assert(sizeof(uintptr_t) == 8); | |
typedef struct Arena Arena; | |
struct Arena { | |
uint8_t *begin; | |
uint8_t *pos; | |
uint8_t *commit; | |
Arena *next_free; | |
}; | |
// * The way an arena works is it allocates the big block of memory and starts allocations at | |
// begin + sizeof(Arena). | |
// * We want this begin to be aligned to 16 bytes so common things like an array (which is always | |
// aligned to 16 bytes here) can be allocated at the start of the arena. | |
// * Arena happens to be 4 pointers so this works well, static assert here to catch if that changes. | |
static_assert(sizeof(Arena) % 16 == 0); | |
// Pool of already allocated arenas that aren't in use. This lets us reuse arenas for small (temp/scratch) allocations | |
// without having to pass them around or pay the cost of allocating a new one each time. | |
thread_local static Arena *arena__free_list; | |
Arena *arena_acquire(); | |
void arena_reset(Arena *arena); | |
void arena_release(Arena *arena); | |
void arena_free(Arena *arena); | |
// Release all the arenas in the pool. | |
// Not really something you'd use in a real program but useful for testing. | |
void arena_free_all(); | |
#define arena_alloc(arena, type) (type*)arena_alloc_size(arena, (ptrdiff_t)sizeof(type), _Alignof(type)) | |
uint8_t* arena_alloc_size(Arena* arena, ptrdiff_t size, ptrdiff_t align); | |
// Get the size of the arena (not including the arena struct itself). | |
ptrdiff_t arena_size(Arena *arena); | |
// You can copy and restore all the data in an area by copying the memory buffer. | |
// This can be very powerful for cloning arbitrary data structures. | |
// If you have a data structure you want to clone and it only uses relative pointers, than you | |
// can dedicate an arena to it and use arena_serialize and arena_deserialize to clone it. | |
// Buf must have space for arena_size(arena) bytes. | |
void arena_serialize(void *buf, Arena *arena); | |
void arena_deserialize(Arena *arena, void *buf, ptrdiff_t size); | |
// Note that dest comes first which matches the order arena_serialize and memcpy use. | |
void arena_clone(Arena *dest, Arena *src); | |
// * It's nice to use relative pointers (offsets from the beginning of the arena) in data structures on an arena. | |
// If you store offsets instead of pointers in all the data structures in an arena, and they all only | |
// point to other things in the arena, you should use relative pointers if you want to take advantage | |
// of the arena_serialize and arena_deserialize. | |
// * Since the type of the offset is always ptrdiff_t you can't know what this is pointing at from the type. | |
// This is annoying, you have to know what it's pointing at from the context. | |
// @todo(steve): Is there a better way to do this in c that doesn't suck? | |
#define rel(arena, ptr) ((ptrdiff_t)(ptr) - (ptrdiff_t)(arena)->begin) | |
#define ptr(arena, off) (void*)((ptrdiff_t)(arena)->begin + (off)) | |
// Slice | |
// * A slice is a pointer and a length. It's a view into an array. | |
// * It's a polymorphic type so you typically typedef it to specific types. | |
// eg: typedef Slice(uint8_t) U8Slice; | |
#define Slice(T) struct { ptrdiff_t len; T *e; } | |
typedef Slice(uint8_t) U8Slice; | |
// * If the slice is a view into data on an arena, and it's stored in a data structure in the arena, | |
// it's better to store a relative slice so the arena can be serialized and deserialized. | |
// * Since the type of the offset is always ptrdiff_t you can't know what this is pointing at from the type. | |
// This is annoying, you have to know what it's pointing at from the context. | |
// @todo(steve): Is there a better way to do this in c that doesn't suck? | |
typedef struct { ptrdiff_t len; ptrdiff_t e; } RelSlice; | |
#define slice_rel(arena, ptr_slice) { .len = (ptr_slice).len, .e = rel(arena, (ptr_slice).e) } | |
#define slice_ptr(arena, rel_slice) { .len = (rel_slice).len, .e = ptr(arena, (rel_slice).e) } | |
#define arena_clone_slice(arena, slice) { \ | |
.len = (slice).len, \ | |
.e = (typeof((slice).e))arena__clone_slice(arena, (U8Slice*)&(slice), (ptrdiff_t)sizeof(*((slice).e))) \ | |
} | |
uint8_t *arena__clone_slice(Arena *arena, U8Slice *slice, ptrdiff_t item_size); | |
// Array | |
// * A dynamic array that grows as needed. | |
// It's backed by an arena so you don't have to free it. | |
// * An array can be resized with helpers like append or setlen. | |
// If the new size is > the capacity it will reallocate the data (or extend the data | |
// allocation if the array is at the end of the arena). | |
// * There is no internal pointer in the array struct, so it works when referenced by other data structures | |
// on the arena using relative pointers. | |
// (of course if it reallocs it will move so you'll have to update the reference). | |
// * It's a polymorphic type so you typically typedef it to specific types. | |
// eg: typedef Array(uint8_t) U8Array; | |
// * Since the type is polymorphic, most of the helper functions are macros. | |
#define Array(T) struct { ptrdiff_t len; ptrdiff_t cap; alignas(16) T e[]; } | |
typedef Array(uint8_t) U8Array; | |
#define arena_alloc_array(arena, type, item_type, cap) \ | |
(type*)arena__alloc_array(arena, (ptrdiff_t)sizeof(item_type), cap) | |
U8Array *arena__alloc_array(Arena *arena, ptrdiff_t item_size, ptrdiff_t cap); | |
U8Array *arena__grow_array(Arena *arena, U8Array *array, ptrdiff_t item_size, ptrdiff_t amount); | |
#define arr_push(arena, array, val) arr__maybegrow(arena, array, 1); (array)->e[(array)->len++] = (val) | |
#define arr_push_array(arena, array, val) \ | |
arr__maybegrow(arena, array, (val)->len); \ | |
( \ | |
xmemcpy(&(array)->e[(array)->len], (val)->e, (val)->len * (ptrdiff_t)sizeof((val)->e[0])), \ | |
(array)->len += (val)->len \ | |
) | |
#define arr_push_slice(arena, array, slice) \ | |
arr__maybegrow(arena, array, (slice).len); \ | |
( \ | |
xmemcpy(&(array)->e[(array)->len], (slice).e, (slice).len * (ptrdiff_t)sizeof((slice).e[0])), \ | |
(array)->len += (slice).len \ | |
) | |
#define arr_setlen(arena, array, n) \ | |
assert(n > 0); \ | |
arr__maybegrow(arena, array, !(array) ? (n) : (n) - (array)->len); \ | |
(array)->len = (n) | |
#define arr_slice(array) {.len = (array)->len, .e = (array)->e} | |
// This isn't strictly necessary for the array or arena arguments. | |
// * For array, you wouldn't push to the result of a function call, | |
// That would just throw away the result. | |
// eg: arr_push(arena, get_array(), 1); | |
// * For arena, it'd be bad to pass arena_acquire() because you'd lose the arena reference and leak the whole thing. | |
// eg: arr_push(arena_acquire(), a, 1); | |
// * It is however important for the n argument in case it's something like ++i | |
// In that case we don't want to evaluate it twice. | |
#define arr__maybegrow(arena, array, n) \ | |
do { \ | |
Arena *_arena = (arena); \ | |
typeof(array) _array = (array); \ | |
ptrdiff_t _n = (n); \ | |
if (!_array || _array->len + _n > _array->cap) { \ | |
_array = (typeof(_array))arena__grow_array(_arena, (U8Array*)_array, (ptrdiff_t)sizeof(_array->e[0]), _n); \ | |
(array) = _array; \ | |
} \ | |
} while (0) | |
#define arena_clone_arr(arena, array) \ | |
(typeof(array))arena__clone_arr(arena, (U8Array*)(array), (ptrdiff_t)sizeof((array)->e[0])) | |
U8Array *arena__clone_arr(Arena *arena, U8Array *array, ptrdiff_t item_size); | |
// Notes on Array vs Slice | |
// * An array contains the array data, so it's a buffer with metadata. The buffer is almost always | |
// in the arena so you typically have pointers to arrays. | |
// * A slice is a pointer with some metadata. That makes it a fancy pointer so you usually use it as a value | |
// rather than a pointer to a slice. | |
// * Many times you'd want to pass an array to a function that takes a slice. Use the slice macro to do that. | |
// * You can't alter slices, so these functions treat them as const. | |
// * There's no slice -> array converter | |
// * There's no guarantees about where the data the slice points to is stored so you don't want to modify it. | |
// * If you want to pass an array and use it as an array, just pass an array pointer. | |
// * If you want to create an array from a slice (by copying the data), you can use arena_push_slice. | |
// A String is a slice of characters. | |
// * It's length doesn't include a null terminator and there's no guarantee that there is a null terminator. | |
// * When it's a view into another string like in a parser or something, there definitely won't be one. | |
// * If it's allocated in an arena with one of the functions here, there usually is a null terminator, | |
// because it's easier to inspect in the debugger. But this is not a property you can rely on in code! | |
// * To pass to stdlib and other functions that expect a null terminated string, you can use the cstr macro. | |
// * It will allocate a new buffer in the arena with a null terminator. | |
// @opt(steve): Don't allocate if the String already has a null terminator. | |
// * To get a String view of a c string, use the str macro. | |
typedef Slice(char) String; | |
#define str(c_string) ((String){ .len = strlen(c_string), .e = c_string }) | |
#define cstr(a, s) arena__alloc_cstring((a), (s)) | |
typedef Array(String) StringArray; | |
typedef Slice(String) StringSlice; | |
StringSlice str_split(Arena *a, String s, char sep); | |
#if 0 | |
// Map | |
// @todo(steve): hashmaps. | |
// probably gonna base it on this either | |
// * jon blow's hashmap: https://gist.github.com/saolsen/25e22c9ec7445acf1a60d484ea357355 | |
// * chris wellon's hashmap: https://nullprogram.com/blog/2023/09/30/ | |
// Interned Strings | |
// @todo(steve): interned strings. | |
// Pool | |
// @todo(steve): pool. | |
typedef struct Pool Pool; | |
struct Pool { | |
Arena *arena; | |
ptrdiff_t len; | |
ptrdiff_t cap; | |
ptrdiff_t item_size; | |
ptrdiff_t next_free_offset; | |
void *free_list; | |
uint8_t *data; | |
}; | |
#endif | |
#endif //STEVE_H | |
#ifdef STEVE_IMPLEMENTATION | |
#include <assert.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <unistd.h> | |
#include <sys/mman.h> | |
#include <stdarg.h> | |
void* xmemcpy(void *dest, const void *src, ptrdiff_t n) { | |
assert(n >= 0); | |
return memcpy(dest, src, (size_t)n); | |
} | |
Arena *arena_acquire() { | |
if (arena__free_list) { | |
Arena *arena = arena__free_list; | |
arena__free_list = arena->next_free; | |
return arena; | |
} | |
// Allocate a new arena. | |
ptrdiff_t pagesize = sysconf(_SC_PAGE_SIZE); // 16kb on my machine. | |
assert(pagesize >= 0); | |
ptrdiff_t cap = pagesize * 4 * 1024 * 1024; // 64GB on my machine. @note(steve): probably way too much | |
assert(cap >= 0); | |
// todo(steve): windows | |
void *r = mmap(NULL, (size_t)cap, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0); | |
if (r == MAP_FAILED) { | |
perror("mmap"); | |
exit(1); | |
} | |
// commit first page | |
if (mprotect(r, (size_t)pagesize, PROT_READ|PROT_WRITE) == -1) { | |
perror("Ran out of virtual memory"); | |
exit(1); | |
} | |
Arena *arena = (Arena *)r; | |
arena->begin = (uint8_t*)(r); | |
arena->pos = (uint8_t*)(r) + sizeof(*arena); | |
arena->commit = (uint8_t*)(r) + pagesize; | |
return arena; | |
} | |
void arena_reset(Arena *arena) { | |
arena->pos = arena->begin + sizeof(*arena); | |
} | |
void arena_release(Arena *arena) { | |
// opt(steve): If we have a lot of pages committed, we could free it | |
// and then create a new virtual allocation the next time it's used. | |
// On windows, you can "uncommit" pages but not on posix. | |
arena_reset(arena); | |
arena->next_free = arena__free_list; | |
arena__free_list = arena; | |
} | |
void arena_free(Arena *arena) { | |
// todo(steve): windows | |
ptrdiff_t pagesize = sysconf(_SC_PAGE_SIZE); | |
assert(pagesize >= 0); | |
if (munmap(arena->begin, (size_t)pagesize * 4 * 1024 * 1024) == -1) { | |
perror("munmap"); | |
exit(1); | |
} | |
} | |
void arena_free_all() { | |
Arena *arena = arena__free_list; | |
while (arena) { | |
Arena *next = arena->next_free; | |
arena_free(arena); | |
arena = next; | |
} | |
arena__free_list = nullptr; | |
} | |
uint8_t* arena_alloc_size(Arena* arena, ptrdiff_t size, ptrdiff_t align) { | |
// Align Pointer | |
uint8_t *start = (uint8_t*)ALIGN_UP_PTR(arena->pos, align); | |
// Commit new page if needed. | |
uint8_t *new_pos = start + size; | |
while (new_pos > arena->commit) { | |
// todo(steve): windows | |
ptrdiff_t pagesize = sysconf(_SC_PAGE_SIZE); | |
assert(pagesize >= 0); | |
if (mprotect(arena->commit, (size_t)pagesize, PROT_READ|PROT_WRITE) == -1) { | |
perror("Ran out of virtual memory"); | |
exit(1); | |
} | |
arena->commit += pagesize; | |
} | |
arena->pos = new_pos; | |
return start; | |
} | |
// The size of the data in the arena. This is the size of buffer you would need to call arena_serialize. | |
ptrdiff_t arena_size(Arena *arena) { | |
return arena->pos - arena->begin - (ptrdiff_t)sizeof(Arena); | |
} | |
void arena_serialize(void *buf, Arena *arena) { | |
xmemcpy(buf, arena->begin + sizeof(Arena), arena_size(arena)); | |
} | |
// Note: This only works on an empty arena, if the arena passed in is not empty it will get reset. | |
// todo(steve): Return an error or something instead of just resetting the arena. | |
void arena_deserialize(Arena *arena, void *buf, ptrdiff_t size) { | |
arena_reset(arena); | |
uint8_t *data = arena_alloc_size(arena, size, 1); | |
xmemcpy(data, buf, size); | |
} | |
void arena_clone(Arena *dest, Arena *src) { | |
ptrdiff_t size = arena_size(src); | |
arena_deserialize(dest, src->begin + sizeof(Arena), size); | |
} | |
uint8_t *arena__clone_slice(Arena *arena, U8Slice *slice, ptrdiff_t item_size) { | |
uint8_t *buf = arena_alloc_size(arena, slice->len * item_size, 16); | |
xmemcpy(buf, slice->e, slice->len * item_size); | |
return buf; | |
} | |
U8Array *arena__alloc_array(Arena *arena, ptrdiff_t item_size, ptrdiff_t cap) { | |
U8Array *array = (U8Array*)arena_alloc_size(arena, (ptrdiff_t)sizeof(U8Array) + item_size * cap, 16); | |
array->len = 0; | |
array->cap = cap; | |
return array; | |
} | |
U8Array *arena__grow_array(Arena *arena, U8Array *array, ptrdiff_t item_size, ptrdiff_t amount) { | |
if (array == nullptr) { | |
return arena__alloc_array(arena, item_size, MAX(4, amount)); | |
} | |
ptrdiff_t new_cap = MAX(array->cap, 4); | |
while (MAX(array->cap, array->len) + amount > new_cap) { | |
new_cap *= 2; | |
} | |
if (new_cap > array->cap) { | |
// Grow the array | |
if (arena->pos == (uint8_t*)(array->e) + array->cap * item_size) { | |
// Array is on the end of the arena, we can just grow it. | |
arena_alloc_size(arena, (new_cap - array->cap) * item_size, 1); | |
array->cap = new_cap; | |
return array; | |
} else { | |
// Array is not on the end of the arena, we need a new allocation. | |
U8Array *new_array = arena__alloc_array(arena, item_size, new_cap); | |
new_array->len = array->len; | |
xmemcpy(new_array->e, array->e, new_array->len * item_size); | |
return new_array; | |
} | |
} | |
return array; | |
} | |
U8Array *arena__clone_arr(Arena *arena, U8Array *array, ptrdiff_t item_size) { | |
U8Array *new_array = arena__alloc_array(arena, item_size, array->len); | |
new_array->len = array->len; | |
xmemcpy(new_array->e, array->e, new_array->len * item_size); | |
return new_array; | |
} | |
char *arena__alloc_cstring(Arena *a, String *s) { | |
char *c = (char*) arena_alloc_size(a, s->len + 1, _Alignof(char)); | |
xmemcpy(c, s->e, s->len); | |
c[s->len] = '\0'; | |
return c; | |
} | |
String format(Arena *a, const char *fmt, ...) { | |
va_list args; | |
va_start(args, fmt); | |
va_list args_copy; | |
va_copy(args_copy, args); | |
ptrdiff_t len = vsnprintf(nullptr, 0, fmt, args_copy); | |
va_end(args_copy); | |
char *c = (char*) arena_alloc_size(a, len + 1, _Alignof(char)); | |
vsnprintf(c, (size_t)len + 1, fmt, args); | |
va_end(args); | |
return (String){ .len = len, .e = c }; | |
} | |
StringSlice str_split(Arena *a, String s, char sep) { | |
// Skip leading separators. | |
int i=0; | |
StringArray *parts = nullptr; | |
while(i < s.len) { | |
int start = i; | |
while(i < s.len && s.e[i] != sep) { | |
i++; | |
} | |
if (i > start) { | |
String part = { .len = i - start, .e = s.e + start }; | |
arr_push(a, parts, part); | |
} | |
while(i < s.len && s.e[i] == sep) { | |
i++; | |
} | |
} | |
if (parts == nullptr) { | |
return (StringSlice){ .len = 0, .e = nullptr }; | |
} | |
StringSlice result = arr_slice(parts); | |
return result; | |
} | |
#endif //STEVE_IMPLEMENTATION | |
#ifdef STEVE_TEST | |
#include <assert.h> | |
#include <stdio.h> | |
#include <string.h> | |
static void test_helpers(); | |
static void test_arena_acquire_release(); | |
static void test_arena_reset(); | |
static void test_arena_free(); | |
static void test_arena_alloc_alignment(); | |
static void test_arena_large_alloc(); | |
static void test_rel_ptr(); | |
static void test_slices(); | |
static void test_rel_slices(); | |
static void test_dynamic_arrays(); | |
static void test_arena_serialize(); | |
static void test_strings(); | |
int main(void) { | |
test_helpers(); | |
test_arena_acquire_release(); | |
test_arena_reset(); | |
test_arena_free(); | |
test_arena_alloc_alignment(); | |
test_arena_large_alloc(); | |
test_rel_ptr(); | |
test_slices(); | |
test_rel_slices(); | |
test_dynamic_arrays(); | |
test_arena_serialize(); | |
test_strings(); | |
printf("steve.h: All tests passed!\n"); | |
return 0; | |
} | |
static void test_helpers() { | |
assert(pow2_next(0) == 0); | |
assert(pow2_next(1) == 1); | |
assert(pow2_next(2) == 2); | |
assert(pow2_next(3) == 4); | |
assert(pow2_next(4) == 4); | |
assert(pow2_next(5) == 8); | |
assert(pow2_next(7) == 8); | |
assert(pow2_next(8) == 8); | |
assert(pow2_next(9) == 16); | |
assert(pow2_next(63) == 64); | |
assert(pow2_next(64) == 64); | |
assert(pow2_next(65) == 128); | |
assert(MIN(10, 20) == 10); | |
assert(MIN(-5, 3) == -5); | |
assert(MAX(10, 20) == 20); | |
assert(MAX(-5, 3) == 3); | |
assert(CLAMP_MAX(25, 20) == 20); // 25 clamped to 20 | |
assert(CLAMP_MAX(15, 20) == 15); // within limit | |
assert(CLAMP_MIN(-1, 0) == 0); // -1 clamped to 0 | |
assert(CLAMP_MIN(10, 0) == 10); // within limit | |
assert(IS_POW2(1) == true); | |
assert(IS_POW2(2) == true); | |
assert(IS_POW2(3) == false); | |
assert(IS_POW2(4) == true); | |
assert(IS_POW2(0) == false); // Edge case: 0 is not a power of 2 | |
assert(ALIGN_UP(0, 8) == 0); | |
assert(ALIGN_UP(1, 8) == 8); | |
assert(ALIGN_UP(7, 8) == 8); | |
assert(ALIGN_UP(8, 8) == 8); | |
assert(ALIGN_UP(9, 8) == 16); | |
assert(ALIGN_DOWN(0, 8) == 0); | |
assert(ALIGN_DOWN(7, 8) == 0); | |
assert(ALIGN_DOWN(8, 8) == 8); | |
assert(ALIGN_DOWN(9, 8) == 8); | |
assert(ALIGN_DOWN(15, 8) == 8); | |
{ | |
uintptr_t ptr_val = 15; | |
void* p1 = (void*)ptr_val; | |
void* aligned_up = ALIGN_UP_PTR(p1, 8); | |
void* aligned_down = ALIGN_DOWN_PTR(p1, 8); | |
assert((uintptr_t)aligned_up == 16); | |
assert((uintptr_t)aligned_down == 8); | |
} | |
} | |
static void test_arena_acquire_release() { | |
// Acquire a single arena and release it | |
Arena *a = arena_acquire(); | |
assert(a != NULL); | |
arena_release(a); | |
// Acquire multiple arenas | |
Arena *a1 = arena_acquire(); | |
Arena *a2 = arena_acquire(); | |
assert(a1 != NULL && a2 != NULL); | |
arena_release(a1); | |
arena_release(a2); | |
// Acquire again to ensure reuse | |
Arena *a3 = arena_acquire(); | |
assert(a3 == a1 || a3 == a2); // Freed arenas should be reused. | |
arena_release(a3); | |
arena_free_all(); | |
} | |
static void test_arena_reset() { | |
Arena *a = arena_acquire(); | |
// Allocate some memory | |
int *x = arena_alloc(a, int); | |
*x = 42; | |
assert(*x == 42); | |
// Reset | |
arena_reset(a); | |
assert(arena_size(a) == 0); | |
// Allocate again after reset | |
int *y = arena_alloc(a, int); | |
*y = 2025; | |
assert(*y == 2025); | |
// Pointers should be to the same memory. | |
assert(y == x); | |
arena_release(a); | |
arena_free_all(); | |
} | |
static void test_arena_free() { | |
Arena *a = arena_acquire(); | |
int *x = arena_alloc(a, int); | |
*x = 123; | |
arena_free(a); // Should succeed without error. | |
// Also test arena_free_all | |
Arena *a1 = arena_acquire(); | |
Arena *a2 = arena_acquire(); | |
arena_release(a1); | |
arena_release(a2); | |
arena_free_all(); | |
assert(arena__free_list == NULL); | |
Arena *a3 = arena_acquire(); | |
arena_release(a3); | |
arena_free_all(); | |
} | |
static void test_arena_alloc_alignment() { | |
Arena *a = arena_acquire(); | |
// Test alignment for 1, 2, 4, 16 | |
ptrdiff_t alignments[] = {1, 2, 4, 16}; | |
for (int i=0; i<4; i++) { | |
ptrdiff_t align = alignments[i]; | |
uint8_t *p = arena_alloc_size(a, 10, align); | |
assert(((ptrdiff_t)p % align) == 0 && "Pointer must be aligned"); | |
} | |
// Cross page boundary test | |
ptrdiff_t pagesize = sysconf(_SC_PAGE_SIZE); | |
(void)arena_alloc_size(a, pagesize + 1, 16); | |
arena_release(a); | |
arena_free_all(); | |
} | |
static void test_arena_large_alloc() { | |
Arena *a = arena_acquire(); | |
ptrdiff_t pagesize = sysconf(_SC_PAGE_SIZE); | |
ptrdiff_t big_size = pagesize * 5; | |
void* big_block = arena_alloc_size(a, big_size, 16); | |
assert(big_block != NULL); | |
// Mke sure we can write to it. | |
memset(big_block, 0xAB, (size_t)big_size); | |
arena_release(a); | |
arena_free_all(); | |
} | |
static void test_rel_ptr() { | |
Arena *a = arena_acquire(); | |
int *x = arena_alloc(a, int); | |
*x = 55; | |
ptrdiff_t offset = rel(a, x); | |
int *y = (int *)ptr(a, offset); | |
assert(x == y); | |
assert(*y == 55); | |
arena_release(a); | |
arena_free_all(); | |
} | |
static void test_slices() { | |
Arena *a = arena_acquire(); | |
// Create an array | |
typedef Array(int) IntArray; | |
IntArray *arr = nullptr; | |
for (int i=0; i<5; i++) { | |
arr_push(a, arr, i*10); | |
} | |
// Turn into slice | |
typedef Slice(int) IntSlice; | |
IntSlice s = arr_slice(arr); | |
assert(s.len == 5); | |
for (int i=0; i<5; i++) { | |
assert(s.e[i] == i*10); | |
} | |
// Clone slice | |
Arena *a2 = arena_acquire(); | |
IntSlice clone = arena_clone_slice(a2, s); | |
assert(clone.len == s.len); | |
for (int i=0; i<5; i++) { | |
assert(clone.e[i] == s.e[i]); | |
} | |
// Mutate original | |
s.e[0] = 999; | |
// Cloned slice remains unaffected | |
assert(clone.e[0] == 0); | |
arena_release(a); | |
arena_release(a2); | |
arena_free_all(); | |
} | |
static void test_rel_slices() { | |
Arena *a = arena_acquire(); | |
// Create an array | |
typedef Array(int) IntArray; | |
IntArray *arr = nullptr; | |
for (int i=0; i<5; i++) { | |
arr_push(a, arr, i*10); | |
} | |
// Turn into slice | |
typedef Slice(int) IntSlice; | |
IntSlice s = arr_slice(arr); | |
assert(s.len == 5); | |
for (int i=0; i<5; i++) { | |
assert(s.e[i] == i*10); | |
} | |
// Turn into relative slice | |
RelSlice rs = slice_rel(a, s); | |
assert(rs.len == 5); | |
// Turn back into slice | |
IntSlice s2 = slice_ptr(a, rs); | |
assert(s2.len == 5); | |
for (int i=0; i<5; i++) { | |
assert(s2.e[i] == i*10); | |
} | |
arena_release(a); | |
arena_free_all(); | |
} | |
static void test_dynamic_arrays() { | |
typedef Array(int) IntArray; | |
typedef Slice(int) IntSlice; | |
Arena *a = arena_acquire(); | |
// alloc_array | |
IntArray *arr = arena_alloc_array(a, IntArray, int, 15); | |
assert(arr->len == 0); | |
assert(arr->cap == 15); | |
// 1. Push elements | |
arr = nullptr; | |
for (int i=0; i<10; i++) { | |
arr_push(a, arr, i); | |
assert(arr->len == i+1); | |
for (int j=0; j<=i; j++) { | |
assert(arr->e[j] == j); | |
} | |
} | |
// 2. Force reallocation | |
// The library starts with capacity=4 (by default) if we push many items, we ensure multiple grows | |
for (int i=0; i<100; i++) { | |
arr_push(a, arr, i + 100); | |
} | |
assert(arr->len == 110); | |
// 3. arr_push_slice | |
IntSlice slice = { .len = 3, .e = (int[]){999, 1000, 1001} }; | |
arr_push_slice(a, arr, slice); | |
assert(arr->len == 113); | |
assert(arr->e[110] == 999); | |
assert(arr->e[111] == 1000); | |
assert(arr->e[112] == 1001); | |
// 4. setlen test | |
// length that puts us on a new page so that we know it worked if we can write to the end. | |
ptrdiff_t pagesize = sysconf(_SC_PAGE_SIZE); | |
arr_setlen(a, arr, pagesize+200); | |
assert(arr->len == pagesize+200); | |
arr->e[pagesize+199] = 1234; | |
// 5. clone_array | |
Arena *a2 = arena_acquire(); | |
IntArray *clone = arena_clone_arr(a2, arr); | |
assert(clone->len == arr->len); | |
for (int i=0; i<(int)arr->len; i++) { | |
assert(clone->e[i] == arr->e[i]); | |
} | |
arena_release(a); | |
arena_release(a2); | |
arena_free_all(); | |
} | |
static void test_arena_serialize() { | |
// @todo(steve): Test relative pointers and relative slices here. | |
Arena *a = arena_acquire(); | |
typedef Array(int) IntArray; | |
typedef Slice(int) IntSlice; | |
IntArray *arr = nullptr; | |
for (int i=0; i<10; i++) { | |
arr_push(a, arr, i*10); | |
} | |
ptrdiff_t arr_rel = rel(a, arr); | |
IntSlice s = arr_slice(arr); | |
RelSlice s_rel = slice_rel(a, s); | |
ptrdiff_t size = arena_size(a); | |
void *buf = malloc((size_t)size); | |
arena_serialize(buf, a); | |
// Deserialize | |
Arena *copy = arena_acquire(); | |
arena_deserialize(copy, buf, size); | |
// Check data | |
IntArray *arr_copy = ptr(copy, arr_rel); | |
IntSlice s_copy = slice_ptr(copy, s_rel); | |
assert(arr_copy != arr); | |
assert((ptrdiff_t)arr_copy >= (ptrdiff_t)copy->begin); | |
assert((ptrdiff_t)arr_copy <= (ptrdiff_t)copy->begin + size); | |
assert(s_copy.len == 10); | |
assert(s_copy.e == arr_copy->e); | |
assert(arr_copy->len == 10); | |
for (int i=0; i<10; i++) { | |
assert(arr_copy->e[i] == i*10); | |
assert(s_copy.e[i] == i*10); | |
} | |
free(buf); | |
Arena *copy2 = arena_acquire(); | |
arena_clone(copy2, a); | |
arr_copy = ptr(copy2, arr_rel); | |
IntSlice s_copy2 = slice_ptr(copy2, s_rel); | |
assert(arr_copy != arr); | |
assert((ptrdiff_t)arr_copy >= (ptrdiff_t)copy2->begin); | |
assert((ptrdiff_t)arr_copy <= (ptrdiff_t)copy2->begin + size); | |
assert(s_copy2.len == 10); | |
assert(s_copy2.e == arr_copy->e); | |
assert(arr_copy->len == 10); | |
for (int i=0; i<10; i++) { | |
assert(arr_copy->e[i] == i*10); | |
assert(s_copy2.e[i] == i*10); | |
} | |
arena_release(a); | |
arena_release(copy); | |
arena_release(copy2); | |
arena_free_all(); | |
} | |
static void test_strings() { | |
Arena *a = arena_acquire(); | |
// format | |
String s1 = format(a, "Hello %d %s", 42, "World"); | |
assert(s1.len == 14); | |
assert(strncmp(s1.e, "Hello 42 World", (size_t)s1.len) == 0); | |
// split | |
String s2 = str("apple,banana,cherry"); | |
StringSlice parts = str_split(a, s2, ','); | |
assert(parts.len == 3); | |
assert(strncmp(parts.e[0].e, "apple", (size_t)parts.e[0].len) == 0); | |
assert(strncmp(parts.e[1].e, "banana", (size_t)parts.e[1].len) == 0); | |
assert(strncmp(parts.e[2].e, "cherry", (size_t)parts.e[2].len) == 0); | |
String s3 = str(",banana,cherry"); | |
parts = str_split(a, s3, ','); | |
assert(parts.len == 2); | |
assert(strncmp(parts.e[0].e, "banana", (size_t)parts.e[1].len) == 0); | |
assert(strncmp(parts.e[1].e, "cherry", (size_t)parts.e[2].len) == 0); | |
String s4 = str("banana,cherry,"); | |
parts = str_split(a, s4, ','); | |
assert(parts.len == 2); | |
assert(strncmp(parts.e[0].e, "banana", (size_t)parts.e[1].len) == 0); | |
assert(strncmp(parts.e[1].e, "cherry", (size_t)parts.e[2].len) == 0); | |
String s5 = str(","); | |
parts = str_split(a, s5, ','); | |
assert(parts.len == 0); | |
String s6 = str(",,,,,"); | |
parts = str_split(a, s6, ','); | |
assert(parts.len == 0); | |
// cstr | |
char *c = cstr(a, &s2); | |
assert(strcmp(c, "apple,banana,cherry") == 0); | |
arena_release(a); | |
arena_free_all(); | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment