Collections
SafeC provides eleven collection modules in std/collections/. Because the compiler does not support generic structs (only generic functions), most collections use void* internally with generic<T> wrapper functions for type-safe access. T is inferred from T* arguments at call sites via monomorphization.
This design keeps a single compiled struct per collection type (no per-T code bloat), while preserving full type safety at every call site — an important property for embedded targets where binary size matters.
ringbuffer is an exception: it is byte-oriented and operates on unsigned char streams directly, using &stack/&static region annotations instead of void*.
slice -- Bounds-Checked Array Access
#include "collections/slice.h"A Slice wraps a typed pointer + length, providing bounds-checked access. The struct stores void* and elem_size; generic functions provide type-safe construction and access.
Struct
struct Slice {
void* ptr; // pointer to first element
unsigned long len; // number of elements
unsigned long elem_size; // stride in bytes
};Type-Erased API
struct Slice slice_void_from(void* ptr, unsigned long len, unsigned long elem_size);
struct Slice slice_void_alloc(unsigned long len, unsigned long elem_size);
void slice_free(struct Slice* s);
int slice_in_bounds(struct Slice s, unsigned long idx);
void* slice_get_raw(struct Slice s, unsigned long idx);
int slice_set_raw(struct Slice s, unsigned long idx, const void* val);
struct Slice slice_sub(struct Slice s, unsigned long start, unsigned long end);
unsigned long slice_len(struct Slice s);
int slice_is_empty(struct Slice s);Generic Array Functions
These generic functions operate on raw T* arrays with explicit length parameters:
generic<T> T* slice_at(T* ptr, unsigned long len, unsigned long idx);
generic<T> void arr_set(T* ptr, unsigned long len, unsigned long idx, T val);
generic<T> T arr_get(T* ptr, unsigned long idx);
generic<T> void arr_fill(T* ptr, unsigned long len, T val);
generic<T> void arr_copy(T* dst, T* src, unsigned long len);
generic<T> T arr_min(T* ptr, unsigned long len);
generic<T> T arr_max(T* ptr, unsigned long len);
generic<T> void arr_reverse(T* ptr, unsigned long len);Example
#include "collections/slice.h"
#include "io.h"
int main() {
int data[5] = {10, 20, 30, 40, 50};
// Bounds-checked access
int* p = slice_at(data, 5, 2); // &data[2]
println_int(*p); // 30
// Fill and reverse
arr_fill(data, 5, 0);
arr_reverse(data, 5);
return 0;
}vec -- Dynamic Array
#include "collections/vec.h"A type-erased dynamic array with automatic growth. O(1) amortized push/pop.
Struct
struct Vec {
void* data; // heap-allocated element buffer
unsigned long len; // current element count
unsigned long cap; // allocated element capacity
unsigned long elem_size; // size of each element in bytes
};Lifecycle
struct Vec vec_new(unsigned long elem_size);
struct Vec vec_with_cap(unsigned long elem_size, unsigned long cap);
void vec_free(struct Vec* v);Capacity
int vec_reserve(struct Vec* v, unsigned long new_cap);
void vec_shrink(struct Vec* v);
unsigned long vec_len(struct Vec* v);
unsigned long vec_cap(struct Vec* v);
int vec_is_empty(struct Vec* v);Element Access (Type-Erased)
void* vec_get_raw(struct Vec* v, unsigned long idx); // NULL if OOB
int vec_set_raw(struct Vec* v, unsigned long idx, const void* elem);
void* vec_front_raw(struct Vec* v); // first element
void* vec_back_raw(struct Vec* v); // last elementMutation
int vec_push(struct Vec* v, const void* elem);
int vec_pop(struct Vec* v, void* out);
int vec_insert(struct Vec* v, unsigned long idx, const void* elem);
int vec_remove(struct Vec* v, unsigned long idx, void* out);
void vec_clear(struct Vec* v);
int vec_extend(struct Vec* v, const void* arr, unsigned long count);Algorithms
void vec_reverse(struct Vec* v);
void vec_sort(struct Vec* v, void* cmp);
long long vec_find(struct Vec* v, const void* key, void* cmp);
int vec_contains(struct Vec* v, const void* key, void* cmp);
struct Vec vec_clone(struct Vec* v);
void vec_foreach(struct Vec* v, void* fn);
struct Vec vec_filter(struct Vec* v, void* pred);
struct Vec vec_map_raw(struct Vec* v, unsigned long out_elem_size, void* fn);cmp:int(*)(const void*, const void*)-- comparator functionfn(foreach):void(*)(void* elem, unsigned long idx)pred(filter):int(*)(const void*)-- return non-zero to keepfn(map):void(*)(const void* in, void* out)
Generic Wrappers
generic<T> int vec_push_t(struct Vec* v, T val);
generic<T> T* vec_at(struct Vec* v, unsigned long idx);
generic<T> int vec_pop_t(struct Vec* v, T* out);
generic<T> struct Vec vec_from_arr(T* arr, unsigned long len);Example
#include "collections/vec.h"
#include "io.h"
int main() {
struct Vec v = vec_new(sizeof(int));
// Push elements using generic wrapper
vec_push_t(&v, 10);
vec_push_t(&v, 20);
vec_push_t(&v, 30);
// Access by index
int* p = vec_at(&v, 1);
println_int(*p); // 20
// Pop last element
int last;
vec_pop_t(&v, &last);
println_int(last); // 30
print("len = ");
println_int(vec_len(&v)); // 2
vec_free(&v);
return 0;
}string -- Mutable String
#include "collections/string.h"A growable, heap-allocated, NUL-terminated byte string with 30+ methods.
Struct
struct String {
char* data; // NUL-terminated buffer
unsigned long len; // byte length (not including NUL)
unsigned long cap; // allocated buffer size (including NUL slot)
};Lifecycle
struct String string_new();
struct String string_from(const char* s);
struct String string_from_n(const char* s, unsigned long n);
struct String string_with_cap(unsigned long cap);
struct String string_repeat(const char* s, unsigned long n);
struct String string_clone(const struct String* s);
void string_free(struct String* s);Access
unsigned long string_len(const struct String* s);
int string_is_empty(const struct String* s);
const char* string_as_ptr(const struct String* s); // NUL-terminated C string
int string_char_at(const struct String* s, unsigned long idx); // -1 if OOB
void string_set_char(struct String* s, unsigned long idx, char c);Append
int string_push_char(struct String* s, char c);
int string_push(struct String* s, const char* cstr);
int string_push_str(struct String* s, const struct String* other);
int string_push_int(struct String* s, long long v);
int string_push_uint(struct String* s, unsigned long long v);
int string_push_float(struct String* s, double v, int decimals);
int string_push_bool(struct String* s, int v);Modification
void string_clear(struct String* s);
void string_truncate(struct String* s, unsigned long new_len);
int string_insert(struct String* s, unsigned long idx, const char* cstr);
int string_remove_range(struct String* s, unsigned long start, unsigned long end);
void string_replace_char(struct String* s, char from, char to);
int string_replace(struct String* s, const char* from, const char* to);
int string_replace_all(struct String* s, const char* from, const char* to);Search
long long string_index_of(const struct String* s, const char* needle);
long long string_last_index_of(const struct String* s, const char* needle);
int string_contains(const struct String* s, const char* needle);
int string_starts_with(const struct String* s, const char* prefix);
int string_ends_with(const struct String* s, const char* suffix);
int string_count(const struct String* s, const char* needle);Transformation (Return New String)
struct String string_substr(const struct String* s, unsigned long start, unsigned long end);
struct String string_to_upper(const struct String* s);
struct String string_to_lower(const struct String* s);
struct String string_trim(const struct String* s);
struct String string_trim_left(const struct String* s);
struct String string_trim_right(const struct String* s);
struct String string_join(const char* sep, const struct String* parts, unsigned long count);Comparison
int string_eq(const struct String* a, const struct String* b);
int string_eq_cstr(const struct String* s, const char* other);
int string_cmp(const struct String* a, const struct String* b); // <0, 0, >0
int string_lt(const struct String* a, const struct String* b);
int string_gt(const struct String* a, const struct String* b);Conversion
long long string_parse_int(const struct String* s, int* ok);
double string_parse_float(const struct String* s, int* ok);Example
#include "collections/string.h"
#include "io.h"
int main() {
struct String s = string_from("Hello");
string_push(&s, ", SafeC!");
println(string_as_ptr(&s)); // Hello, SafeC!
// Search
if (string_contains(&s, "SafeC")) {
println("Found SafeC");
}
// Transform
struct String upper = string_to_upper(&s);
println(string_as_ptr(&upper)); // HELLO, SAFEC!
// Build a number string
struct String num = string_new();
string_push(&num, "value = ");
string_push_int(&num, 42);
println(string_as_ptr(&num)); // value = 42
string_free(&num);
string_free(&upper);
string_free(&s);
return 0;
}stack -- LIFO Stack
#include "collections/stack.h"A last-in-first-out stack backed by a growable array. O(1) amortized push/pop.
Struct
struct Stack {
void* data;
unsigned long top; // number of elements
unsigned long cap;
unsigned long elem_size;
};API
// Lifecycle
struct Stack stack_new(unsigned long elem_size);
struct Stack stack_with_cap(unsigned long elem_size, unsigned long cap);
void stack_free(struct Stack* s);
// Core operations
int stack_push(struct Stack* s, const void* elem); // 1 on success
int stack_pop(struct Stack* s, void* out); // 0 if empty
void* stack_peek(struct Stack* s); // NULL if empty
unsigned long stack_len(struct Stack* s);
int stack_is_empty(struct Stack* s);
void stack_clear(struct Stack* s);Generic Wrappers
generic<T> int stack_push_t(struct Stack* s, T val);
generic<T> T* stack_peek_t(struct Stack* s);
generic<T> int stack_pop_t(struct Stack* s, T* out);Example
#include "collections/stack.h"
#include "io.h"
int main() {
struct Stack s = stack_new(sizeof(int));
stack_push_t(&s, 10);
stack_push_t(&s, 20);
stack_push_t(&s, 30);
int* top = stack_peek_t(&s);
println_int(*top); // 30
int val;
stack_pop_t(&s, &val);
println_int(val); // 30
stack_free(&s);
return 0;
}queue -- FIFO Queue
#include "collections/queue.h"A first-in-first-out circular buffer queue. Amortized O(1) enqueue/dequeue. Grows automatically when full.
Struct
struct Queue {
void* data;
unsigned long head; // index of front element
unsigned long tail; // index where next element will be written
unsigned long len; // current element count
unsigned long cap; // total allocated slots
unsigned long elem_size;
};API
// Lifecycle
struct Queue queue_new(unsigned long elem_size);
struct Queue queue_with_cap(unsigned long elem_size, unsigned long cap);
void queue_free(struct Queue* q);
// Core operations
int queue_enqueue(struct Queue* q, const void* elem);
int queue_dequeue(struct Queue* q, void* out);
void* queue_front(struct Queue* q); // peek front; NULL if empty
void* queue_back(struct Queue* q); // peek back; NULL if empty
unsigned long queue_len(struct Queue* q);
int queue_is_empty(struct Queue* q);
void queue_clear(struct Queue* q);Generic Wrappers
generic<T> int queue_enqueue_t(struct Queue* q, T val);
generic<T> T* queue_front_t(struct Queue* q);
generic<T> int queue_dequeue_t(struct Queue* q, T* out);Example
#include "collections/queue.h"
#include "io.h"
int main() {
struct Queue q = queue_new(sizeof(int));
queue_enqueue_t(&q, 1);
queue_enqueue_t(&q, 2);
queue_enqueue_t(&q, 3);
int val;
queue_dequeue_t(&q, &val);
println_int(val); // 1 (FIFO order)
int* front = queue_front_t(&q);
println_int(*front); // 2
queue_free(&q);
return 0;
}list -- Doubly Linked List
#include "collections/list.h"A doubly linked list with push/pop on both ends, search, removal, and in-order traversal.
Structs
struct ListNode {
void* data;
struct ListNode* next;
struct ListNode* prev;
};
struct List {
struct ListNode* head;
struct ListNode* tail;
unsigned long len;
unsigned long elem_size;
};API
// Lifecycle
struct List list_new(unsigned long elem_size);
void list_free(struct List* l);
// Push / Pop
int list_push_front(struct List* l, const void* elem);
int list_push_back(struct List* l, const void* elem);
int list_pop_front(struct List* l, void* out);
int list_pop_back(struct List* l, void* out);
void* list_front(struct List* l); // NULL if empty
void* list_back(struct List* l); // NULL if empty
unsigned long list_len(struct List* l);
int list_is_empty(struct List* l);
void list_clear(struct List* l);
// Search & Iteration
struct ListNode* list_find(struct List* l, const void* val, void* cmp);
int list_contains(struct List* l, const void* val, void* cmp);
void list_remove_node(struct List* l, struct ListNode* node);
int list_remove(struct List* l, const void* val, void* cmp);
void list_foreach(struct List* l, void* fn); // fn: void(*)(void* data)
// Reorder
void list_reverse(struct List* l);cmp:int(*)(const void*, const void*)-- comparator
Generic Wrappers
generic<T> int list_push_back_t(struct List* l, T val);
generic<T> T* list_front_t(struct List* l);
generic<T> T* list_back_t(struct List* l);Example
#include "collections/list.h"
#include "io.h"
int main() {
struct List l = list_new(sizeof(int));
list_push_back_t(&l, 10);
list_push_back_t(&l, 20);
list_push_front_t(&l, 5);
int* front = list_front_t(&l);
println_int(*front); // 5
int* back = list_back_t(&l);
println_int(*back); // 20
list_reverse(&l);
front = list_front_t(&l);
println_int(*front); // 20
list_free(&l);
return 0;
}map -- Hash Map
#include "collections/map.h"An open-addressing hash map with linear probing and a djb2 hash function. Load factor threshold is 0.75; resizes automatically. Keys are compared byte-by-byte (memcmp). Use the str_map_* variants for C-string keys.
Structs
struct MapEntry {
void* key;
void* val;
unsigned int hash;
int state; // 0=empty, 1=occupied, 2=tombstone
};
struct HashMap {
struct MapEntry* buckets;
unsigned long cap; // must be power of 2
unsigned long len; // live entries
unsigned long key_size;
unsigned long val_size;
};Byte-Key API
// Lifecycle
struct HashMap map_new(unsigned long key_size, unsigned long val_size);
struct HashMap map_with_cap(unsigned long key_size, unsigned long val_size, unsigned long cap);
void map_free(struct HashMap* m);
// Core operations
int map_insert(struct HashMap* m, const void* key, const void* val);
void* map_get(struct HashMap* m, const void* key); // NULL if missing
int map_contains(struct HashMap* m, const void* key);
int map_remove(struct HashMap* m, const void* key);
unsigned long map_len(struct HashMap* m);
int map_is_empty(struct HashMap* m);
void map_clear(struct HashMap* m);
void map_foreach(struct HashMap* m, void* fn); // fn: void(*)(const void* key, void* val)String-Key Convenience
For maps keyed by const char* strings:
struct HashMap str_map_new(unsigned long val_size);
int str_map_insert(struct HashMap* m, const char* key, const void* val);
void* str_map_get(struct HashMap* m, const char* key);
int str_map_contains(struct HashMap* m, const char* key);
int str_map_remove(struct HashMap* m, const char* key);Generic Wrappers
generic<T> int map_insert_t(struct HashMap* m, const void* key, T val);
generic<T> T* map_get_t(struct HashMap* m, const void* key);Example
#include "collections/map.h"
#include "io.h"
int main() {
// Integer-keyed map
struct HashMap m = map_new(sizeof(int), sizeof(int));
int key = 42;
int val = 100;
map_insert(&m, &key, &val);
int* found = map_get(&m, &key);
if (found != 0) {
println_int(*found); // 100
}
map_free(&m);
// String-keyed map
struct HashMap sm = str_map_new(sizeof(double));
double pi = 3.14159;
str_map_insert(&sm, "pi", &pi);
double* p = str_map_get(&sm, "pi");
if (p != 0) {
println_float(*p); // 3.14159
}
map_free(&sm);
return 0;
}btree -- Ordered B-Tree Map
#include "collections/btree.h"A pool-based B-tree (order 4) backed by a 256-node static pool. O(log n) insert/lookup. All keys are unsigned long; values are void*. Provides sorted in-order traversal.
Struct
struct BTree {
// 256-node pool (1-indexed; 0 = null sentinel)
// internal FL/SL bitmaps (opaque)
}API
struct BTree btree_new();
void btree_clear(struct BTree* t);
int btree_insert(struct BTree* t, unsigned long key, void* val);
void* btree_get(struct BTree* t, unsigned long key); // NULL if missing
int btree_contains(struct BTree* t, unsigned long key);
void btree_foreach(struct BTree* t, void* fn);
// fn: void(*)(unsigned long key, void* val) — called in ascending key orderGeneric Wrappers
generic<T> int btree_insert_t(struct BTree* t, unsigned long key, T val);
generic<T> T* btree_get_t(struct BTree* t, unsigned long key);Example
#include "collections/btree.h"
#include "io.h"
void print_entry(unsigned long key, void* val) {
print_int(key); print(" -> "); println_int(*(int*)val);
}
int main() {
struct BTree t = btree_new();
int v10 = 100, v20 = 200, v5 = 50;
btree_insert(&t, 10, &v10);
btree_insert(&t, 20, &v20);
btree_insert(&t, 5, &v5);
int* found = btree_get_t(&t, 10);
println_int(*found); // 100
// In-order traversal: 5, 10, 20
btree_foreach(&t, print_entry);
return 0;
}ringbuffer -- SPSC Lock-Free Ring Buffer
#include "collections/ringbuffer.h"A single-producer / single-consumer byte-oriented power-of-two ring buffer. Uses compiler barriers for correct producer/consumer ordering without OS locks. Suitable for ISR-to-task data transfer and audio pipelines.
Unlike the other collection types, RingBuffer is not element-based — it operates on raw byte streams. Use it with unsigned char buffers.
Struct
struct RingBuffer {
&static unsigned char buf; // backing store — must be static-lifetime
unsigned long cap; // capacity in bytes (must be power of two)
unsigned long mask; // cap - 1 (for fast modulo)
volatile unsigned long head; // write position (producer)
volatile unsigned long tail; // read position (consumer)
unsigned long readable() const; // bytes available to read
unsigned long writable() const; // bytes that can be written
int is_empty() const;
int is_full() const;
unsigned long write(const &stack unsigned char data, unsigned long len);
unsigned long read(&stack unsigned char out, unsigned long len);
unsigned long peek(&stack unsigned char out, unsigned long len) const;
unsigned long discard(unsigned long len);
void clear();
}
// Initialise with an existing static-lifetime backing store.
// `cap` must be a power of two.
struct RingBuffer ring_init(&static unsigned char buf, unsigned long cap);Static macro
The RING_STATIC macro is the idiomatic way to create a ring buffer with no heap allocation:
// Declare + initialise a 256-byte ring buffer backed by a static array
RING_STATIC(uart_rx, 256);
// Expands to:
// static unsigned char uart_rx_storage_[256];
// static struct RingBuffer uart_rx = { uart_rx_storage_, 256, 255, 0, 0 };Example
#include "collections/ringbuffer.h"
#include "io.h"
// Static 128-byte buffer — no heap required
RING_STATIC(rb, 128);
int main() {
// Producer: write bytes
unsigned char tx[] = {'H', 'e', 'l', 'l', 'o'};
rb.write(tx, 5);
print("readable: ");
println_int(rb.readable()); // 5
// Consumer: read bytes back
unsigned char rx[5];
rb.read(rx, 5);
int i = 0;
while (i < 5) { print_char(rx[i]); i = i + 1; }
println(""); // Hello
return 0;
}INFO
buf has region &static unsigned char — the backing store must outlive the RingBuffer struct itself. Use RING_STATIC for embedded globals. For heap-backed use, cast inside an unsafe {} block and supply your own lifetime discipline.
static_collections -- Zero-Heap Compile-Time Collections
#include "collections/static_vec.h"Header-only macros that declare fixed-capacity collections on the stack or as static globals. No heap allocation, no function call overhead.
Static Vec
STATIC_VEC_DECL(MyVec, int, 32); // declares: struct MyVec { int data[32]; int len; }
STATIC_VEC_INIT(v, MyVec); // MyVec v = {0}
STATIC_VEC_PUSH(v, 42); // v.data[v.len++] = 42 (no bounds check elided)
STATIC_VEC_POP(v); // v.len--
STATIC_VEC_TOP(v); // v.data[v.len - 1]
STATIC_VEC_AT(v, i); // v.data[i]Static Map (Open-Addressing Hash)
STATIC_MAP_DECL(MyMap, unsigned long, int, 64); // key=ulong, val=int, 64 buckets
STATIC_MAP_INIT(m, MyMap);
STATIC_MAP_INSERT(m, key, val); // insert or update
STATIC_MAP_GET(m, key, MyMap); // returns pointer to val, or NULLExample
#include "collections/static_vec.h"
#include "io.h"
STATIC_VEC_DECL(IntVec, int, 16);
int main() {
IntVec v;
STATIC_VEC_INIT(v, IntVec);
STATIC_VEC_PUSH(v, 10);
STATIC_VEC_PUSH(v, 20);
STATIC_VEC_PUSH(v, 30);
println_int(STATIC_VEC_TOP(v)); // 30
STATIC_VEC_POP(v);
println_int(STATIC_VEC_TOP(v)); // 20
for (int i = 0; i < v.len; i++) {
println_int(STATIC_VEC_AT(v, i)); // 10, 20
}
return 0;
}bst -- Binary Search Tree
#include "collections/bst.h"An unbalanced binary search tree using a user-supplied comparator function. Keys and values are heap-copied. Provides in-order traversal (sorted ascending).
Structs
struct BSTNode {
void* key;
void* val;
struct BSTNode* left;
struct BSTNode* right;
};
struct BST {
struct BSTNode* root;
unsigned long key_size;
unsigned long val_size;
void* cmp_fn; // int(*)(const void*, const void*)
unsigned long len;
};API
// Lifecycle
struct BST bst_new(unsigned long key_size, unsigned long val_size, void* cmp_fn);
void bst_free(struct BST* t);
// Core operations
int bst_insert(struct BST* t, const void* key, const void* val);
void* bst_get(struct BST* t, const void* key); // NULL if not found
int bst_contains(struct BST* t, const void* key);
int bst_remove(struct BST* t, const void* key);
unsigned long bst_len(struct BST* t);
int bst_is_empty(struct BST* t);
void bst_clear(struct BST* t);
// Min / Max
void* bst_min_key(struct BST* t); // NULL if empty
void* bst_max_key(struct BST* t); // NULL if empty
// Traversal (in-order = sorted ascending)
void bst_foreach_inorder(struct BST* t, void* fn); // fn: void(*)(const void* key, void* val)Built-In Comparators
Pass these as the cmp_fn argument:
int bst_cmp_int(const void* a, const void* b); // int keys
int bst_cmp_ll(const void* a, const void* b); // long long keys
int bst_cmp_str(const void* a, const void* b); // const char* keys
int bst_cmp_uint(const void* a, const void* b); // unsigned int keysGeneric Wrappers
generic<T> int bst_insert_t(struct BST* t, const void* key, T val);
generic<T> T* bst_get_t(struct BST* t, const void* key);Example
#include "collections/bst.h"
#include "io.h"
int main() {
// Integer-keyed BST
struct BST tree = bst_new(sizeof(int), sizeof(int), bst_cmp_int);
int k1 = 30; int v1 = 300;
int k2 = 10; int v2 = 100;
int k3 = 50; int v3 = 500;
bst_insert(&tree, &k1, &v1);
bst_insert(&tree, &k2, &v2);
bst_insert(&tree, &k3, &v3);
int* found = bst_get(&tree, &k2);
if (found != 0) {
println_int(*found); // 100
}
// Min and max keys
int* min_k = bst_min_key(&tree);
int* max_k = bst_max_key(&tree);
print("min = ");
println_int(*min_k); // 10
print("max = ");
println_int(*max_k); // 50
bst_free(&tree);
return 0;
}