various small standalone c utility libraries. c code is in src/c-precompiled. sc versions are in src/sc. the libraries are developed in sc and then translated to normal, readable c, formatted with clang-format
- array: structs for arrays that include size or a count of used content
- futures: fine-grained parallelism with objects that can be waited on for results
- hashtable: hash-tables for any key/value type
- memreg: track heap memory allocations in function scope
- memory: allocate or register memory using status_t
- queue: a queue for any data type
- quicksort: a generic implementation for arrays of any type
- random: pseudo random number generation
- set: sets for any key/value type
- spline-path: interpolated 2d paths between given points
- status: return-status and error handling using a tiny object with status/error id and source library id
- test: minimalistic test runner macros
- thread-pool: task queue with pthread threads and wait conditions for pausing inactive threads
- mi-list: a basic, macro-based linked list
- ikv: file format for nested named arrays, for example for text configuration
- one: miscellaneous helpers
- guile: a few helpers for working with guile
code is under gpl3+, documentation under cc-by-nc.
- include "sph/library_name.h" (if exists) if the .c file is included in a shared library being used
- include "sph/library_name.c" for the rest of the code
macro that defines a generic dynamic array type for arbitrary element types.
- structure layout:
{ .data, .size, .used }
.data
points to allocated memory.size
is the allocated element capacity.used
tracks how many elements are currently stored- supports variable-length data within a fixed-size memory block
- uses
malloc
andfree
by default; custom allocators may be substituted - fixed capacity unless explicitly resized
- minimal code size and suitable for inclusion-based builds
- c standard library:
stdlib.h
,inttypes.h
#include "sph/array.h"
sph_array_declare_type(my_type, int);
int main(void) {
status_declare;
my_type_t a;
status_require(my_type_new(4, &a));
sph_array_add(a, 1);
sph_array_add(a, 2);
for (size_t i = 0; i < a.used; i += 1) {
sph_array_get(a, i);
}
my_type_free(a);
exit:
status_return;
}
sph_array_declare_type
defines:
// returns 0 on success, 1 if allocation failed
uint8_t name##_new(size_t initial_size, name##_t *result);
// increases capacity, preserving data
uint8_t name##_resize(name##_t *a, size_t new_size);
// adds a value; may resize internally if supported
void sph_array_add(name##_t a, element_type value);
// retrieves element at index
element_type sph_array_get(name##_t a, size_t index);
// releases allocated memory
void name##_free(name##_t a);
- each declared array type is named
name##_t
- the array may be resized or reused without reallocating a new object
.used
can be reset manually for reuse- memory allocation is linear in element size and capacity
provides task objects with functions that are executed in parallel with a thread-pool. calling touch on an object waits for its completion and returns its result. depends on thread-pool.c
#include <inttypes.h>
#include "queue.h"
#include "thread-pool.h"
#include "thread-pool.c"
#include "futures.c"
void* future_work(void* data) {
// return value is a void pointer.
// just for example this returns a new object. the data could be modified instead
uint8_t* a;
a = malloc(sizeof(uint8_t));
*a = 2 + *(uint8_t*)(data);
return a;
};
int main() {
future_t* future;
uint8_t data;
uint8_t* result;
data = 8;
// this must be called at least once somewhere before futures can be used
future_init(10);
// create a new future and get the result later with touch
future = future_new(future_work, &data);
if(!future) return 1;
result = (uint8_t*)(touch(future));
// result is 10
free(result);
// this frees the thread-pool in case futures are not needed anymore in the process or before exit
future_deinit();
return 0;
}
gcc "$c/test/thread-pool.c" -o tmp/test-thread-pool -lpthread -D _DEFAULT_SOURCE
macro that defines open-addressing hash-table data structures for arbitrary key/value types.
- c standard library (
stdlib.h
,string.h
,inttypes.h
)
- linear probing for collision resolution
- separate arrays for flags, keys, and values
- flags encode slot state, so no sentinel key or null value is needed
- lookups and insertions operate in constant expected time
- minimal implementation (<150 loc)
- tables are fixed-size after creation; reallocation requires constructing a new table and reinserting entries
#include "sph/hashtable.h"
// name, key_type, value_type, hash_function, equal_function, size_factor
sph_hashtable_declare_type(mytype, uint64_t, uint32_t,
sph_hashtable_hash_integer, sph_hashtable_equal_integer, 2);
int main(void) {
mytype_t ht;
if (mytype_new(200, &ht)) return 1;
mytype_set(ht, 44, 5);
mytype_get(ht, 44);
mytype_remove(ht, 44);
mytype_free(ht);
return 0;
}
sph_hashtable_declare_type
generates these functions
(##
denotes token concatenation; e.g., name##_new
→ mytype_new
):
// returns 0 on success, 1 on allocation failure
uint8_t name##_new(size_t min_size, name##_t *result);
// returns pointer to inserted or existing value, 0 if table full
value_type *name##_set(name##_t a, key_type key, value_type value);
// returns pointer to stored value, 0 if not found
value_type *name##_get(name##_t a, key_type key);
// returns 0 if removed, 1 if not found
uint8_t name##_remove(name##_t a, key_type key);
void name##_free(name##_t a);
example hash and equality macros:
#define sph_hashtable_hash_integer(key, size) ((key) % (size))
#define sph_hashtable_equal_integer(a, b) ((a) == (b))
track memory allocations locally on the stack and free all allocations up to point easily.
- memreg: fixed store, caller-sized
- memreg2: fixed store, caller-sized, custom free handler
- memreg-heap: heap-owned, fixed-size unless resized manually
- memreg-grow: heap-owned, auto-growing, ensure semantics, multiple stores, per-entry handler
#include "sph/memreg.c"
int main() {
memreg_init(2);
int* data_a = malloc(12 * sizeof(int));
if(!data_a) goto exit; // have to free nothing
memreg_add(data_a);
// more code ...
char* data_b = malloc(20 * sizeof(char));
if(!data_b) goto exit; // have to free "data_a"
memreg_add(data_b);
// ...
if(is_error) goto exit; // have to free "data_a" and "data_b"
// ...
exit:
memreg_free;
return 0;
}
introduces two local variables: memreg_register, an array for addresses, and memreg_index, the next index. memreg_init size must be a static value
memreg.c
also contains a *_named variant that supports multiple concurrent registers identified by name
memreg_init_named(testname, 1);
memreg_add_named(testname, &variable);
memreg_free_named(testname);
helpers for error and return status code handling with a routine local goto label and a tiny status object that includes the status id and an id for the library it belongs to, for when multiple libraries can return possibly overlapping error codes
status_t is defined as follows
typedef struct {
int id;
uint8_t* group;
} status_t;
group is a string, otherwise it would be more difficult to not conflict with groups used by other libraries
status_t test() {
status_declare;
if (1 < 2) {
int group_id = "test";
int error_id = 456;
status_set_goto(group_id, error_id);
}
exit:
return status;
}
int main() {
status_declare;
// code ...
status_require(test());
// more code ...
exit:
return status.id;
}
status_require
goes directly to exit if the returned status_t
from test()
isnt status_id_success
status_declare: declares a variable with name `status` of type `status_t`
status_group_undefined
status_i_require(expression): `status.id = expression`, otherwise like status_require
status_id_success: 0
status_is_failure: true if status.id is zero
status_is_success: true if status.id is non-zero
status_require(expression): evaluates `status = expression` and jumps to exit on error
status_return: same as `return status`
status_set(group_id, status_id): `status.group = group_id; status.id = status_id;`
status_set_goto(group_id, status_id): like status_set but goes to exit
status_t: struct
id: int
group: uint8_t*
macro that defines open-addressing set data structures for arbitrary value types.
- linear probing for lookup and insertion
- backward-shift deletion to preserve probe continuity
- expected constant-time insert, remove, and lookup
- fixed capacity; to expand, create a larger set and reinsert values
- occupancy tracked with a bitset, no tombstone markers
- supports one special "null" value as a valid element using a nullable flag
- c standard library:
stdlib.h
,string.h
,inttypes.h
#include "sph/set.h"
// name, value_type, hash, equal, null_value, size_factor
sph_set_declare_type(myset, int, sph_set_hash_integer,
sph_set_equal_integer, 0, 2);
int main(void) {
myset_t a;
if (myset_new(3, &a)) return 1;
myset_add(&a, 3);
myset_add(&a, 5);
myset_remove(&a, 3);
myset_get(a, 5);
myset_free(a);
return 0;
}
sph_set_declare_type
defines these functions:
// returns 0 on success, 1 if memory allocation failed
uint8_t name##_new(size_t min_size, name##_t *result);
// clears all entries
void name##_clear(name##_t *a);
// returns a pointer to the stored value, or 0 if not found
// if the value equals the null value, returns a.values if present, otherwise 0
value_type *name##_get(name##_t a, value_type value);
// inserts the value if not present and returns its address, or 0 if no space remains
// if the value equals the null value, sets a->nullable and returns a->values
value_type *name##_add(name##_t *a, value_type value);
// removes the value and returns 0 on success, 1 if not found
// if the value equals the null value, clears a->nullable
uint8_t name##_remove(name##_t *a, value_type value);
void name##_free(name##_t a);
example hash and equality macros:
#define sph_set_hash_integer(value, size) ((value) % (size))
#define sph_set_equal_integer(a, b) ((a) == (b))
- declared type:
name##_t
contains.size
,.mask
,.values
,.occupied
,.nullable
- the null element, if present, is tracked through
.nullable
, and its storage address isa.values
- the number of elements allocated equals the next power of two greater than or equal to
(size_factor Ă— requested_size)
- the hash function must return an index within the range from zero up to but not including the table size
a basic linked list with custom element types.
- the c standard library (stdlib.h and inttypes.h)
mi_list_declare_type(list_64, uint64_t);
list_64_t* a;
a = list_64_add(a, 112);
mi_list_first(a);
mi_list_first_address(a);
mi_list_rest(a);
list_64_length(a);
list_64_destroy(a);
{name}_add
{name}_destroy
{name}_drop
{name}_length
{name}_struct
{name}_t
mi_list_first
mi_list_first_address
mi_list_rest
{name}_t is a struct:
typedef struct name##_struct {
struct name##_struct* link;
element_type data;
} name##_t;
a fifo queue with the operations enqueue and dequeue that can enqueue structs of mixed types. elements need to be a struct with a field queue_node_t with a specific name. a queue_node_t object to be added must not already be in the queue. the queue will use and reference the queue_node field and does not need to allocate new memory. depends on queue.c
typedef struct {
// custom field definitions ...
queue_node_t queue_node;
} element_t;
element_t e;
queue_t q;
queue_init(&q);
queue_enq(&q, &e.queue_node);
queue_get(queue_deq(&q), element_t, queue_node);
if(0 = q.size) { printf("it's empty\n"); }
convenient testing with just a few tiny macros.
each assertion and test prints its result to standard output.
#include "sph/status.h"
#include "sph/test.h"
status_t test_example() {
test_helper_assert("my assertion", 1 == 1)
exit:
status_return;
}
int main() {
status_declare;
test_helper_test_one(test_example);
exit:
test_helper_display_summary();
return status.id;
}
#include <inttypes.h>
#include "sph/queue.h"
#include "sph/thread-pool.c"
void work(thread_pool_task_t* task) {
// ... do things in parallel ...
// task can be freed here
free(task);
};
int main() {
int error;
sph_thread_pool_t pool;
sph_thread_pool_task_t* task;
error = sph_thread_pool_new(10, &pool);
if(error) return error;
task = malloc(sizeof(sph_thread_pool_task_t));
if(!task) return 1;
task->f = work;
sph_thread_pool_enqueue(a, task);
// when the thread pool is not needed anymore then all threads can be closed.
// arguments: thread_pool, no_wait, discard_queue
sph_thread_pool_finish(&pool, 0, 0);
return 0;
}
spline-path creates discrete 2d paths with segments that interpolate between given points. paths can be composed to construct more complex paths.
implemented segment types and interpolation methods
- linear: draws lines between points
- bezier: 4 point cubic bezier interpolation
- move: gap, moves the point the next segment starts on
- constant: repeats the last value as a flat line to infinity
- path: another spline path as a segment
- power: power-curve interpolation. 'y = y0 + (y1 - y0) * t ** gamma'
- exponential: 'y = y0 + (y1 - y0) * ((e ** (gamma * t) - 1) / (e ** gamma - 1))'
features
- extremely fast through only supporting 2d, with only selected and optimized interpolators, limited segment count and sampling portions of paths at once instead of only single points
- maps from one independent discrete value to one dependent continuous value and only the dependent value is returned
- paths are stateless and the same path object can be used by multiple threads
- multidimensional interpolation can be archieved with separate calls for additional dimensions
- originally developed for dsp and the sampling of many paths 96000 times per second in parallel
example of a path that begins at x 10, draws a line to x 20, a bezier curve to x 40 and any y value after that will be 25. paths start at (0, 0) and every segment gives the target point to draw to, like lineTo and similar in svg paths.
int main() {
// declaration
spline_path_value_t out[50];
spline_path_time_t i;
spline_path_t path;
spline_path_point_t p;
spline_path_segment_t s;
spline_path_segment_t segments[4];
spline_path_segment_count_t segments_len;
// path segment configuration.
s.interpolator = spline_path_i_move;
p.x = 10;
p.y = 5;
(s.points)[0] = p;
segments[0] = s;
s.interpolator = spline_path_i_line;
p.x = 20;
p.y = 10;
(s.points)[0] = p;
segments[1] = s;
s.interpolator = spline_path_i_bezier;
p.x = 25;
p.y = 15;
(s.points)[0] = p;
p.x = 30;
p.y = 20;
(s.points)[1] = p;
p.x = 40;
p.y = 25;
(s.points)[2] = p;
segments[2] = s;
s.interpolator = spline_path_i_constant;
segments[3] = s;
segments_len = 4;
// path object creation
if(spline_path_new(segments_len, segments, &path)) return 1;
// get points on the path, a range of points at once
spline_path_get(path, 5, 25, out);
spline_path_get(path, 25, 55, 20 + out);
// display the extracted points
for (i = 0; (i < 50); i = (1 + i)) {
printf("%lu %f\n", i, (out[i]));
};
spline_path_free(path);
}
works with arrays of structs and any other array type.
void quicksort(
uint8_t (*less_p)(void*, ssize_t, ssize_t),
void (*swap)(void*, ssize_t, ssize_t),
void* array,
ssize_t left,
ssize_t right);
#include <stdio.h>
#include <inttypes.h>
#include <sys/types.h>
#include "sph/quicksort.c"
uint8_t uint32_less_p(void* a, ssize_t b, ssize_t c) {
// typecast "a" to the right pointer type and compare elements at indices b and c
return (((uint32_t*)(a))[b] < ((uint32_t*)(a))[c]);
}
void uint32_swapper(void* a, ssize_t b, ssize_t c) {
uint32_t d;
d = ((uint32_t*)(a))[b];
((uint32_t*)(a))[b] = ((uint32_t*)(a))[c];
((uint32_t*)(a))[c] = d;
}
#define test_element_count 100;
int main() {
// prepare input
size_t i;
uint32_t uint32_array[test_element_count];
for (i = 0; (i < test_element_count); i = 1 + i) {
uint32_array[i] = test_element_count - i;
};
// sort
quicksort(uint32_less_p, uint32_swapper, uint32_array, 0, test_element_count - 1);
// display results
for (i = 0; (i < test_element_count); i = 1 + i) {
printf("%u", uint32_array[i]);
};
return 0;
};
xoshiro256** and xoshiro256+ implementation (article about the algorithm here). unbiased bounding (lemire, o'neill).
#include <inttypes.h>
#include "sph/random.h"
#include "sph/random.c"
sph_random_state_t s = sph_random_state_new(11223344);
uint64_t a = sph_random_u64(&s);
double b = sph_random_f64(&s);
uint64_t c = sph_random_u64_bounded(&s, 10);
uint64_t a_array[100];
double b_array[100];
uint64_t c_array[100];
sph_random_u64_array(&s, 100, a_array);
sph_random_f64_array(&s, 100, b_array);
sph_random_u64_bounded_array(&s, 100, 10, c_array);
sph_random_f64 :: sph_random_state_t*:state -> double
sph_random_f64_array :: sph_random_state_t*:state size_t:size double*:out -> void
sph_random_state_new :: uint64_t:seed -> sph_random_state_t
sph_random_u64 :: sph_random_state_t*:state -> uint64_t
sph_random_u64_array :: sph_random_state_t*:state size_t:size uint64_t*:out -> void
sph_random_u64_bounded :: sph_random_state_t*:state uint64_t:range -> uint64_t
sph_random_u64_bounded_array :: sph_random_state_t*:state uint64_t:range size_t:size uint64_t*:out -> void
rotl(x, k)
sph_random_f64_from_u64(a)
sph_random_state_t: struct
data: array uint64_t 4
indent-key-value - file format and hashtable type for possibly nested named arrays. use case: text configuration file format. the parser is written so that it should be relatively easy to add custom value types.
depends on stdio.h, inttypes.h, murmur3.c, sph/status.c and sph/hashtable.c.
it uses getline which with gcc needs #define _GNU_SOURCE
before including stdio.h.
- one key/value association per line
- key and values separated by space
- values can be integers, reals or strings
- associations can be nested by using two spaces of indentation in lines subsequent to keys that have no value
key1 0 1 2 3 4
key2 0.1 5 6.33 7 8
key3 string1 string2 string3
nest1
nest11
nest111 string4 string5
nest12 9
nest13 string6
key4 string7
#define _GNU_SOURCE
#include <stdio.h>
#include <inttypes.h>
#include <sph/status.h>
#include <sph/hashtable.c>
#include <murmur3.c>
#include <sph/ikv.h>
#include <sph/ikv.c>
int main() {
ikv_t a;
ikv_t b;
ikv_value_t* value;
status_declare;
status_i_require(ikv_new(100, &a));
/* read/write */
status_require(ikv_read_file("other/ikv-test-data", a));
ikv_write_file(a, "tmp/ikv-test");
ikv_free_all(a);
status_i_require(ikv_new(100, &a));
status_require(ikv_read_file("tmp/ikv-test", a));
/* access top level */
value = ikv_get(a, "key4");
printf("key4 string: %s\n", ikv_value_get_string(value, 0));
value = ikv_get(a, "key3");
printf("key3 string: %s\n", ikv_value_get_string(value, 2));
/* access nested */
value = ikv_get(a, "nest1");
b = ikv_value_get_ikv(value);
value = ikv_get(b, "nest11");
value = ikv_get((ikv_value_get_ikv(value)), "nest111");
printf("nest111 string: %s\n", ikv_value_get_string(value, 0));
value = ikv_get(b, "nest12");
printf("nest12 integer %lu\n", ikv_value_get_integer(value, 0));
ikv_free_all(a);
exit:
return status.id;
}
ikv_floats_new :: size_t:size ikv_float_t**:out -> status_t
ikv_free_all :: ikv_t:a -> void
ikv_hash_64 :: ikv_key_t*:key size_t:size -> uint64_t
ikv_integers_new :: size_t:size ikv_integer_t**:out -> status_t
ikv_read_file :: ikv_string_t*:path ikv_t:ikv -> status_t
ikv_read_indent :: FILE*:file ikv_read_value_t:read_value ikv_t:ikv -> status_t
ikv_read_value :: char*:line size_t:size ikv_value_t*:value -> status_t
ikv_write_file :: ikv_t:a ikv_string_t*:path -> void
ikv_write_file_direct :: ikv_t:a FILE*:file ikv_nesting_t:nesting -> void
ikv_equal(a, b)
ikv_float_t
ikv_integer_t
ikv_key_t
ikv_max_keysize
ikv_max_nesting
ikv_memory_error
ikv_nesting_t
ikv_s_group_ikv
ikv_s_id_file_open_failed
ikv_s_id_full
ikv_s_id_memory
ikv_string_t
ikv_type_floats
ikv_type_ikv
ikv_type_integers
ikv_type_strings
ikv_type_t
ikv_value_get_float(ikv_value_t*:a, index)
ikv_value_get_ikv(a)
ikv_value_get_integer(a, index)
ikv_value_get_string(a, index)
ikv_read_value_t: char* size_t ikv_value_t* -> status_t
ikv_value_t: struct
type: ikv_type_t
size: ikv_integer_t
data: void*
- read/write from/to strings