update ucx default tip

Sun, 05 Jan 2025 22:00:39 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 05 Jan 2025 22:00:39 +0100
changeset 440
7c4b9cba09ca
parent 439
bf7084544cb1

update ucx

ucx/Makefile file | annotate | diff | comparison | revisions
ucx/allocator.c file | annotate | diff | comparison | revisions
ucx/array_list.c file | annotate | diff | comparison | revisions
ucx/buffer.c file | annotate | diff | comparison | revisions
ucx/compare.c file | annotate | diff | comparison | revisions
ucx/cx/allocator.h file | annotate | diff | comparison | revisions
ucx/cx/array_list.h file | annotate | diff | comparison | revisions
ucx/cx/buffer.h file | annotate | diff | comparison | revisions
ucx/cx/collection.h file | annotate | diff | comparison | revisions
ucx/cx/common.h file | annotate | diff | comparison | revisions
ucx/cx/compare.h file | annotate | diff | comparison | revisions
ucx/cx/hash_key.h file | annotate | diff | comparison | revisions
ucx/cx/hash_map.h file | annotate | diff | comparison | revisions
ucx/cx/iterator.h file | annotate | diff | comparison | revisions
ucx/cx/json.h file | annotate | diff | comparison | revisions
ucx/cx/linked_list.h file | annotate | diff | comparison | revisions
ucx/cx/list.h file | annotate | diff | comparison | revisions
ucx/cx/map.h file | annotate | diff | comparison | revisions
ucx/cx/mempool.h file | annotate | diff | comparison | revisions
ucx/cx/printf.h file | annotate | diff | comparison | revisions
ucx/cx/properties.h file | annotate | diff | comparison | revisions
ucx/cx/streams.h file | annotate | diff | comparison | revisions
ucx/cx/string.h file | annotate | diff | comparison | revisions
ucx/cx/test.h file | annotate | diff | comparison | revisions
ucx/cx/tree.h file | annotate | diff | comparison | revisions
ucx/hash_key.c file | annotate | diff | comparison | revisions
ucx/hash_map.c file | annotate | diff | comparison | revisions
ucx/iterator.c file | annotate | diff | comparison | revisions
ucx/json.c file | annotate | diff | comparison | revisions
ucx/linked_list.c file | annotate | diff | comparison | revisions
ucx/list.c file | annotate | diff | comparison | revisions
ucx/map.c file | annotate | diff | comparison | revisions
ucx/mempool.c file | annotate | diff | comparison | revisions
ucx/printf.c file | annotate | diff | comparison | revisions
ucx/properties.c file | annotate | diff | comparison | revisions
ucx/streams.c file | annotate | diff | comparison | revisions
ucx/string.c file | annotate | diff | comparison | revisions
ucx/szmul.c file | annotate | diff | comparison | revisions
ucx/tree.c file | annotate | diff | comparison | revisions
ucx/utils.c file | annotate | diff | comparison | revisions
ui/common/context.c file | annotate | diff | comparison | revisions
ui/common/document.c file | annotate | diff | comparison | revisions
ui/common/menu.c file | annotate | diff | comparison | revisions
ui/common/object.c file | annotate | diff | comparison | revisions
ui/common/types.c file | annotate | diff | comparison | revisions
ui/gtk/container.c file | annotate | diff | comparison | revisions
ui/gtk/dnd.c file | annotate | diff | comparison | revisions
ui/gtk/list.c file | annotate | diff | comparison | revisions
ui/gtk/menu.c file | annotate | diff | comparison | revisions
ui/motif/button.c file | annotate | diff | comparison | revisions
--- a/ucx/Makefile	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/Makefile	Sun Jan 05 22:00:39 2025 +0100
@@ -26,7 +26,6 @@
 # POSSIBILITY OF SUCH DAMAGE.
 #
 
-BUILD_ROOT = ../
 include ../config.mk
 
 # list of source files
@@ -37,14 +36,16 @@
 SRC += compare.c
 SRC += hash_key.c
 SRC += hash_map.c
+SRC += iterator.c
 SRC += linked_list.c
 SRC += list.c
 SRC += map.c
 SRC += printf.c
 SRC += string.c
-SRC += utils.c
 SRC += tree.c
-SRC += iterator.c
+SRC += streams.c
+SRC += properties.c
+SRC += json.c
 
 OBJ   = $(SRC:%.c=../build/ucx/%$(OBJ_EXT))
 
--- a/ucx/allocator.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/allocator.c	Sun Jan 05 22:00:39 2025 +0100
@@ -28,35 +28,33 @@
 
 #include "cx/allocator.h"
 
-__attribute__((__malloc__, __alloc_size__(2)))
+#include <errno.h>
+
 static void *cx_malloc_stdlib(
-        __attribute__((__unused__)) void *d,
+        cx_attr_unused void *d,
         size_t n
 ) {
     return malloc(n);
 }
 
-__attribute__((__warn_unused_result__, __alloc_size__(3)))
 static void *cx_realloc_stdlib(
-        __attribute__((__unused__)) void *d,
+        cx_attr_unused void *d,
         void *mem,
         size_t n
 ) {
     return realloc(mem, n);
 }
 
-__attribute__((__malloc__, __alloc_size__(2, 3)))
 static void *cx_calloc_stdlib(
-        __attribute__((__unused__)) void *d,
+        cx_attr_unused void *d,
         size_t nelem,
         size_t n
 ) {
     return calloc(nelem, n);
 }
 
-__attribute__((__nonnull__))
 static void cx_free_stdlib(
-        __attribute__((__unused__)) void *d,
+        cx_attr_unused void *d,
         void *mem
 ) {
     free(mem);
@@ -75,20 +73,41 @@
 };
 CxAllocator *cxDefaultAllocator = &cx_default_allocator;
 
-
+#undef cx_reallocate
 int cx_reallocate(
         void **mem,
         size_t n
 ) {
     void *nmem = realloc(*mem, n);
     if (nmem == NULL) {
-        return 1;
+        return 1; // LCOV_EXCL_LINE
     } else {
         *mem = nmem;
         return 0;
     }
 }
 
+#undef cx_reallocatearray
+int cx_reallocatearray(
+        void **mem,
+        size_t nmemb,
+        size_t size
+) {
+    size_t n;
+    if (cx_szmul(nmemb, size, &n)) {
+        errno = EOVERFLOW;
+        return 1;
+    } else {
+        void *nmem = realloc(*mem, n);
+        if (nmem == NULL) {
+            return 1; // LCOV_EXCL_LINE
+        } else {
+            *mem = nmem;
+            return 0;
+        }
+    }
+}
+
 // IMPLEMENTATION OF HIGH LEVEL API
 
 void *cxMalloc(
@@ -106,6 +125,22 @@
     return allocator->cl->realloc(allocator->data, mem, n);
 }
 
+void *cxReallocArray(
+        const CxAllocator *allocator,
+        void *mem,
+        size_t nmemb,
+        size_t size
+) {
+    size_t n;
+    if (cx_szmul(nmemb, size, &n)) {
+        errno = EOVERFLOW;
+        return NULL;
+    } else {
+        return allocator->cl->realloc(allocator->data, mem, n);
+    }
+}
+
+#undef cxReallocate
 int cxReallocate(
         const CxAllocator *allocator,
         void **mem,
@@ -113,7 +148,23 @@
 ) {
     void *nmem = allocator->cl->realloc(allocator->data, *mem, n);
     if (nmem == NULL) {
-        return 1;
+        return 1; // LCOV_EXCL_LINE
+    } else {
+        *mem = nmem;
+        return 0;
+    }
+}
+
+#undef cxReallocateArray
+int cxReallocateArray(
+        const CxAllocator *allocator,
+        void **mem,
+        size_t nmemb,
+        size_t size
+) {
+    void *nmem = cxReallocArray(allocator, *mem, nmemb, size);
+    if (nmem == NULL) {
+        return 1; // LCOV_EXCL_LINE
     } else {
         *mem = nmem;
         return 0;
--- a/ucx/array_list.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/array_list.c	Sun Jan 05 22:00:39 2025 +0100
@@ -30,6 +30,7 @@
 #include "cx/compare.h"
 #include <assert.h>
 #include <string.h>
+#include <errno.h>
 
 // Default array reallocator
 
@@ -37,65 +38,258 @@
         void *array,
         size_t capacity,
         size_t elem_size,
-        __attribute__((__unused__)) struct cx_array_reallocator_s *alloc
+        cx_attr_unused CxArrayReallocator *alloc
 ) {
-    return realloc(array, capacity * elem_size);
+    size_t n;
+    if (cx_szmul(capacity, elem_size, &n)) {
+        errno = EOVERFLOW;
+        return NULL;
+    }
+    return realloc(array, n);
 }
 
-struct cx_array_reallocator_s cx_array_default_reallocator_impl = {
+CxArrayReallocator cx_array_default_reallocator_impl = {
         cx_array_default_realloc, NULL, NULL, 0, 0
 };
 
-struct cx_array_reallocator_s *cx_array_default_reallocator = &cx_array_default_reallocator_impl;
+CxArrayReallocator *cx_array_default_reallocator = &cx_array_default_reallocator_impl;
+
+// Stack-aware array reallocator
+
+static void *cx_array_advanced_realloc(
+        void *array,
+        size_t capacity,
+        size_t elem_size,
+        cx_attr_unused CxArrayReallocator *alloc
+) {
+    // check for overflow
+    size_t n;
+    if (cx_szmul(capacity, elem_size, &n)) {
+        errno = EOVERFLOW;
+        return NULL;
+    }
+
+    // retrieve the pointer to the actual allocator
+    const CxAllocator *al = alloc->ptr1;
+
+    // check if the array is still located on the stack
+    void *newmem;
+    if (array == alloc->ptr2) {
+        newmem = cxMalloc(al, n);
+        if (newmem != NULL && array != NULL) {
+            memcpy(newmem, array, n);
+        }
+    } else {
+        newmem = cxRealloc(al, array, n);
+    }
+    return newmem;
+}
+
+struct cx_array_reallocator_s cx_array_reallocator(
+        const struct cx_allocator_s *allocator,
+        const void *stackmem
+) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
+    }
+    return (struct cx_array_reallocator_s) {
+            cx_array_advanced_realloc,
+            (void*) allocator, (void*) stackmem,
+            0, 0
+    };
+}
 
 // LOW LEVEL ARRAY LIST FUNCTIONS
 
-enum cx_array_result cx_array_copy(
+static size_t cx_array_align_capacity(
+        size_t cap,
+        size_t alignment,
+        size_t max
+) {
+    if (cap > max - alignment) {
+        return cap;
+    } else {
+        return cap - (cap % alignment) + alignment;
+    }
+}
+
+int cx_array_reserve(
+        void **array,
+        void *size,
+        void *capacity,
+        unsigned width,
+        size_t elem_size,
+        size_t elem_count,
+        CxArrayReallocator *reallocator
+) {
+    // assert pointers
+    assert(array != NULL);
+    assert(size != NULL);
+    assert(capacity != NULL);
+
+    // default reallocator
+    if (reallocator == NULL) {
+        reallocator = cx_array_default_reallocator;
+    }
+
+    // determine size and capacity
+    size_t oldcap;
+    size_t oldsize;
+    size_t max_size;
+    if (width == 0 || width == sizeof(size_t)) {
+        oldcap = *(size_t*) capacity;
+        oldsize = *(size_t*) size;
+        max_size = SIZE_MAX;
+    } else if (width == sizeof(uint16_t)) {
+        oldcap = *(uint16_t*) capacity;
+        oldsize = *(uint16_t*) size;
+        max_size = UINT16_MAX;
+    } else if (width == sizeof(uint8_t)) {
+        oldcap = *(uint8_t*) capacity;
+        oldsize = *(uint8_t*) size;
+        max_size = UINT8_MAX;
+    }
+#if CX_WORDSIZE == 64
+    else if (width == sizeof(uint32_t)) {
+        oldcap = *(uint32_t*) capacity;
+        oldsize = *(uint32_t*) size;
+        max_size = UINT32_MAX;
+    }
+#endif
+    else {
+        errno = EINVAL;
+        return 1;
+    }
+
+    // assert that the array is allocated when it has capacity
+    assert(*array != NULL || oldcap == 0);
+
+    // check for overflow
+    if (elem_count > max_size - oldsize) {
+        errno = EOVERFLOW;
+        return 1;
+    }
+
+    // determine new capacity
+    size_t newcap = oldsize + elem_count;
+
+    // reallocate if possible
+    if (newcap > oldcap) {
+        // calculate new capacity (next number divisible by 16)
+        newcap = cx_array_align_capacity(newcap, 16, max_size);
+
+        // perform reallocation
+        void *newmem = reallocator->realloc(
+                *array, newcap, elem_size, reallocator
+        );
+        if (newmem == NULL) {
+            return 1; // LCOV_EXCL_LINE
+        }
+
+        // store new pointer
+        *array = newmem;
+
+        // store new capacity
+        if (width == 0 || width == sizeof(size_t)) {
+            *(size_t*) capacity = newcap;
+        } else if (width == sizeof(uint16_t)) {
+            *(uint16_t*) capacity = (uint16_t) newcap;
+        } else if (width == sizeof(uint8_t)) {
+            *(uint8_t*) capacity = (uint8_t) newcap;
+        }
+#if CX_WORDSIZE == 64
+        else if (width == sizeof(uint32_t)) {
+            *(uint32_t*) capacity = (uint32_t) newcap;
+        }
+#endif
+    }
+
+    return 0;
+}
+
+int cx_array_copy(
         void **target,
-        size_t *size,
-        size_t *capacity,
+        void *size,
+        void *capacity,
+        unsigned width,
         size_t index,
         const void *src,
         size_t elem_size,
         size_t elem_count,
-        struct cx_array_reallocator_s *reallocator
+        CxArrayReallocator *reallocator
 ) {
     // assert pointers
     assert(target != NULL);
     assert(size != NULL);
+    assert(capacity != NULL);
     assert(src != NULL);
 
-    // determine capacity
-    size_t cap = capacity == NULL ? *size : *capacity;
+    // default reallocator
+    if (reallocator == NULL) {
+        reallocator = cx_array_default_reallocator;
+    }
+
+    // determine size and capacity
+    size_t oldcap;
+    size_t oldsize;
+    size_t max_size;
+    if (width == 0 || width == sizeof(size_t)) {
+        oldcap = *(size_t*) capacity;
+        oldsize = *(size_t*) size;
+        max_size = SIZE_MAX;
+    } else if (width == sizeof(uint16_t)) {
+        oldcap = *(uint16_t*) capacity;
+        oldsize = *(uint16_t*) size;
+        max_size = UINT16_MAX;
+    } else if (width == sizeof(uint8_t)) {
+        oldcap = *(uint8_t*) capacity;
+        oldsize = *(uint8_t*) size;
+        max_size = UINT8_MAX;
+    }
+#if CX_WORDSIZE == 64
+    else if (width == sizeof(uint32_t)) {
+        oldcap = *(uint32_t*) capacity;
+        oldsize = *(uint32_t*) size;
+        max_size = UINT32_MAX;
+    }
+#endif
+    else {
+        errno = EINVAL;
+        return 1;
+    }
+
+    // assert that the array is allocated when it has capacity
+    assert(*target != NULL || oldcap == 0);
+
+    // check for overflow
+    if (index > max_size || elem_count > max_size - index) {
+        errno = EOVERFLOW;
+        return 1;
+    }
 
     // check if resize is required
     size_t minsize = index + elem_count;
-    size_t newsize = *size < minsize ? minsize : *size;
-    bool needrealloc = newsize > cap;
+    size_t newsize = oldsize < minsize ? minsize : oldsize;
 
     // reallocate if possible
-    if (needrealloc) {
-        // a reallocator and a capacity variable must be available
-        if (reallocator == NULL || capacity == NULL) {
-            return CX_ARRAY_REALLOC_NOT_SUPPORTED;
-        }
-
+    size_t newcap = oldcap;
+    if (newsize > oldcap) {
         // check, if we need to repair the src pointer
         uintptr_t targetaddr = (uintptr_t) *target;
         uintptr_t srcaddr = (uintptr_t) src;
         bool repairsrc = targetaddr <= srcaddr
-                         && srcaddr < targetaddr + cap * elem_size;
+                         && srcaddr < targetaddr + oldcap * elem_size;
 
         // calculate new capacity (next number divisible by 16)
-        cap = newsize - (newsize % 16) + 16;
-        assert(cap > newsize);
+        newcap = cx_array_align_capacity(newsize, 16, max_size);
+        assert(newcap > newsize);
 
         // perform reallocation
         void *newmem = reallocator->realloc(
-                *target, cap, elem_size, reallocator
+                *target, newcap, elem_size, reallocator
         );
         if (newmem == NULL) {
-            return CX_ARRAY_REALLOC_FAILED;
+            return 1;
         }
 
         // repair src pointer, if necessary
@@ -103,9 +297,8 @@
             src = ((char *) newmem) + (srcaddr - targetaddr);
         }
 
-        // store new pointer and capacity
+        // store new pointer
         *target = newmem;
-        *capacity = cap;
     }
 
     // determine target pointer
@@ -113,14 +306,34 @@
     start += index * elem_size;
 
     // copy elements and set new size
+    // note: no overflow check here, b/c we cannot get here w/o allocation
     memmove(start, src, elem_count * elem_size);
-    *size = newsize;
+
+    // if any of size or capacity changed, store them back
+    if (newsize != oldsize || newcap != oldcap) {
+        if (width == 0 || width == sizeof(size_t)) {
+            *(size_t*) capacity = newcap;
+            *(size_t*) size = newsize;
+        } else if (width == sizeof(uint16_t)) {
+            *(uint16_t*) capacity = (uint16_t) newcap;
+            *(uint16_t*) size = (uint16_t) newsize;
+        } else if (width == sizeof(uint8_t)) {
+            *(uint8_t*) capacity = (uint8_t) newcap;
+            *(uint8_t*) size = (uint8_t) newsize;
+        }
+#if CX_WORDSIZE == 64
+        else if (width == sizeof(uint32_t)) {
+            *(uint32_t*) capacity = (uint32_t) newcap;
+            *(uint32_t*) size = (uint32_t) newsize;
+        }
+#endif
+    }
 
     // return successfully
-    return CX_ARRAY_SUCCESS;
+    return 0;
 }
 
-enum cx_array_result cx_array_insert_sorted(
+int cx_array_insert_sorted(
         void **target,
         size_t *size,
         size_t *capacity,
@@ -128,7 +341,7 @@
         const void *sorted_data,
         size_t elem_size,
         size_t elem_count,
-        struct cx_array_reallocator_s *reallocator
+        CxArrayReallocator *reallocator
 ) {
     // assert pointers
     assert(target != NULL);
@@ -136,25 +349,35 @@
     assert(capacity != NULL);
     assert(cmp_func != NULL);
     assert(sorted_data != NULL);
-    assert(reallocator != NULL);
+
+    // default reallocator
+    if (reallocator == NULL) {
+        reallocator = cx_array_default_reallocator;
+    }
 
     // corner case
     if (elem_count == 0) return 0;
 
+    // overflow check
+    if (elem_count > SIZE_MAX - *size) {
+        errno = EOVERFLOW;
+        return 1;
+    }
+
     // store some counts
     size_t old_size = *size;
     size_t needed_capacity = old_size + elem_count;
 
     // if we need more than we have, try a reallocation
     if (needed_capacity > *capacity) {
-        size_t new_capacity = needed_capacity - (needed_capacity % 16) + 16;
+        size_t new_capacity = cx_array_align_capacity(needed_capacity, 16, SIZE_MAX);
         void *new_mem = reallocator->realloc(
                 *target, new_capacity, elem_size, reallocator
         );
         if (new_mem == NULL) {
             // give it up right away, there is no contract
             // that requires us to insert as much as we can
-            return CX_ARRAY_REALLOC_FAILED;
+            return 1;  // LCOV_EXCL_LINE
         }
         *target = new_mem;
         *capacity = new_capacity;
@@ -228,7 +451,7 @@
     // still buffer elements left?
     // don't worry, we already moved them to the correct place
 
-    return CX_ARRAY_SUCCESS;
+    return 0;
 }
 
 size_t cx_array_binary_search_inf(
@@ -255,6 +478,9 @@
         return 0;
     }
 
+    // special case: there is only one element and that is smaller
+    if (size == 1) return 0;
+
     // check the last array element
     result = cmp_func(elem, array + elem_size * (size - 1));
     if (result >= 0) {
@@ -287,10 +513,48 @@
     return result < 0 ? (pivot_index - 1) : pivot_index;
 }
 
+size_t cx_array_binary_search(
+        const void *arr,
+        size_t size,
+        size_t elem_size,
+        const void *elem,
+        cx_compare_func cmp_func
+) {
+    size_t index = cx_array_binary_search_inf(
+            arr, size, elem_size, elem, cmp_func
+    );
+    if (index < size &&
+            cmp_func(((const char *) arr) + index * elem_size, elem) == 0) {
+        return index;
+    } else {
+        return size;
+    }
+}
+
+size_t cx_array_binary_search_sup(
+        const void *arr,
+        size_t size,
+        size_t elem_size,
+        const void *elem,
+        cx_compare_func cmp_func
+) {
+    size_t inf = cx_array_binary_search_inf(
+            arr, size, elem_size, elem, cmp_func
+    );
+    if (inf == size) {
+        // no infimum means, first element is supremum
+        return 0;
+    } else if (cmp_func(((const char *) arr) + inf * elem_size, elem) == 0) {
+        return inf;
+    } else {
+        return inf + 1;
+    }
+}
+
 #ifndef CX_ARRAY_SWAP_SBO_SIZE
 #define CX_ARRAY_SWAP_SBO_SIZE 128
 #endif
-unsigned cx_array_swap_sbo_size = CX_ARRAY_SWAP_SBO_SIZE;
+const unsigned cx_array_swap_sbo_size = CX_ARRAY_SWAP_SBO_SIZE;
 
 void cx_array_swap(
         void *arr,
@@ -337,22 +601,9 @@
     struct cx_list_s base;
     void *data;
     size_t capacity;
-    struct cx_array_reallocator_s reallocator;
+    CxArrayReallocator reallocator;
 } cx_array_list;
 
-static void *cx_arl_realloc(
-        void *array,
-        size_t capacity,
-        size_t elem_size,
-        struct cx_array_reallocator_s *alloc
-) {
-    // retrieve the pointer to the list allocator
-    const CxAllocator *al = alloc->ptr1;
-
-    // use the list allocator to reallocate the memory
-    return cxRealloc(al, array, capacity * elem_size);
-}
-
 static void cx_arl_destructor(struct cx_list_s *list) {
     cx_array_list *arl = (cx_array_list *) list;
 
@@ -394,10 +645,11 @@
         size_t elems_to_move = list->collection.size - index;
         size_t start_of_moved = index + n;
 
-        if (CX_ARRAY_SUCCESS != cx_array_copy(
+        if (cx_array_copy(
                 &arl->data,
                 &list->collection.size,
                 &arl->capacity,
+                0,
                 start_of_moved,
                 first_to_move,
                 list->collection.elem_size,
@@ -414,20 +666,21 @@
     // therefore, it is impossible to leave this function with an invalid array
 
     // place the new elements
-    if (CX_ARRAY_SUCCESS == cx_array_copy(
+    if (cx_array_copy(
             &arl->data,
             &list->collection.size,
             &arl->capacity,
+            0,
             index,
             array,
             list->collection.elem_size,
             n,
             &arl->reallocator
     )) {
-        return n;
-    } else {
         // array list implementation is "all or nothing"
         return 0;
+    } else {
+        return n;
     }
 }
 
@@ -439,7 +692,7 @@
     // get a correctly typed pointer to the list
     cx_array_list *arl = (cx_array_list *) list;
 
-    if (CX_ARRAY_SUCCESS == cx_array_insert_sorted(
+    if (cx_array_insert_sorted(
             &arl->data,
             &list->collection.size,
             &arl->capacity,
@@ -449,10 +702,10 @@
             n,
             &arl->reallocator
     )) {
-        return n;
-    } else {
         // array list implementation is "all or nothing"
         return 0;
+    } else {
+        return n;
     }
 }
 
@@ -494,45 +747,66 @@
     }
 }
 
-static int cx_arl_remove(
+static size_t cx_arl_remove(
         struct cx_list_s *list,
-        size_t index
+        size_t index,
+        size_t num,
+        void *targetbuf
 ) {
     cx_array_list *arl = (cx_array_list *) list;
 
     // out-of-bounds check
+    size_t remove;
     if (index >= list->collection.size) {
-        return 1;
+        remove = 0;
+    } else if (index + num > list->collection.size) {
+        remove = list->collection.size - index;
+    } else {
+        remove = num;
     }
 
-    // content destruction
-    cx_invoke_destructor(list, ((char *) arl->data) + index * list->collection.elem_size);
+    // easy exit
+    if (remove == 0) return 0;
 
-    // short-circuit removal of last element
-    if (index == list->collection.size - 1) {
-        list->collection.size--;
-        return 0;
+    // destroy or copy contents
+    if (targetbuf == NULL) {
+        for (size_t idx = index; idx < index + remove; idx++) {
+            cx_invoke_destructor(
+                    list,
+                    ((char *) arl->data) + idx * list->collection.elem_size
+            );
+        }
+    } else {
+        memcpy(
+                targetbuf,
+                ((char *) arl->data) + index * list->collection.elem_size,
+                remove * list->collection.elem_size
+        );
     }
 
-    // just move the elements starting at index to the left
-    int result = cx_array_copy(
+    // short-circuit removal of last elements
+    if (index + remove == list->collection.size) {
+        list->collection.size -= remove;
+        return remove;
+    }
+
+    // just move the elements to the left
+    cx_array_copy(
             &arl->data,
             &list->collection.size,
             &arl->capacity,
+            0,
             index,
-            ((char *) arl->data) + (index + 1) * list->collection.elem_size,
+            ((char *) arl->data) + (index + remove) * list->collection.elem_size,
             list->collection.elem_size,
-            list->collection.size - index - 1,
+            list->collection.size - index - remove,
             &arl->reallocator
     );
 
-    // cx_array_copy cannot fail, array cannot grow
-    assert(result == 0);
+    // decrease the size
+    list->collection.size -= remove;
 
-    // decrease the size
-    list->collection.size--;
-
-    return 0;
+    return remove;
 }
 
 static void cx_arl_clear(struct cx_list_s *list) {
@@ -594,10 +868,11 @@
     for (ssize_t i = 0; i < (ssize_t) list->collection.size; i++) {
         if (0 == list->collection.cmpfunc(elem, cur)) {
             if (remove) {
-                if (0 == cx_arl_remove(list, i)) {
+                if (1 == cx_arl_remove(list, i, 1, NULL)) {
                     return i;
                 } else {
-                    return -1;
+                    // should be unreachable
+                    return -1;  // LCOV_EXCL_LINE
                 }
             } else {
                 return i;
@@ -664,7 +939,7 @@
     struct cx_iterator_s *iter = it;
     if (iter->base.remove) {
         iter->base.remove = false;
-        cx_arl_remove(iter->src_handle.m, iter->index);
+        cx_arl_remove(iter->src_handle.m, iter->index, 1, NULL);
     } else {
         iter->index++;
         iter->elem_handle =
@@ -678,7 +953,7 @@
     const cx_array_list *list = iter->src_handle.c;
     if (iter->base.remove) {
         iter->base.remove = false;
-        cx_arl_remove(iter->src_handle.m, iter->index);
+        cx_arl_remove(iter->src_handle.m, iter->index, 1, NULL);
     }
     iter->index--;
     if (iter->index < list->base.collection.size) {
@@ -754,14 +1029,13 @@
 
     // allocate the array after the real elem_size is known
     list->data = cxCalloc(allocator, initial_capacity, elem_size);
-    if (list->data == NULL) {
+    if (list->data == NULL) { // LCOV_EXCL_START
         cxFree(allocator, list);
         return NULL;
-    }
+    } // LCOV_EXCL_STOP
 
     // configure the reallocator
-    list->reallocator.realloc = cx_arl_realloc;
-    list->reallocator.ptr1 = (void *) allocator;
+    list->reallocator = cx_array_reallocator(allocator, NULL);
 
     return (CxList *) list;
 }
--- a/ucx/buffer.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/buffer.c	Sun Jan 05 22:00:39 2025 +0100
@@ -27,10 +27,21 @@
  */
 
 #include "cx/buffer.h"
-#include "cx/utils.h"
 
 #include <stdio.h>
 #include <string.h>
+#include <errno.h>
+
+static int buffer_copy_on_write(CxBuffer* buffer) {
+    if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) return 0;
+    void *newspace = cxMalloc(buffer->allocator, buffer->capacity);
+    if (NULL == newspace) return -1;
+    memcpy(newspace, buffer->space, buffer->size);
+    buffer->space = newspace;
+    buffer->flags &= ~CX_BUFFER_COPY_ON_WRITE;
+    buffer->flags |= CX_BUFFER_FREE_CONTENTS;
+    return 0;
+}
 
 int cxBufferInit(
         CxBuffer *buffer,
@@ -39,13 +50,18 @@
         const CxAllocator *allocator,
         int flags
 ) {
-    if (allocator == NULL) allocator = cxDefaultAllocator;
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
+    }
+    if (flags & CX_BUFFER_COPY_ON_EXTEND) {
+        flags |= CX_BUFFER_AUTO_EXTEND;
+    }
     buffer->allocator = allocator;
     buffer->flags = flags;
     if (!space) {
         buffer->bytes = cxMalloc(allocator, capacity);
         if (buffer->bytes == NULL) {
-            return 1;
+            return -1; // LCOV_EXCL_LINE
         }
         buffer->flags |= CX_BUFFER_FREE_CONTENTS;
     } else {
@@ -55,19 +71,27 @@
     buffer->size = 0;
     buffer->pos = 0;
 
-    buffer->flush_func = NULL;
-    buffer->flush_target = NULL;
-    buffer->flush_blkmax = 0;
-    buffer->flush_blksize = 4096;
-    buffer->flush_threshold = SIZE_MAX;
+    buffer->flush = NULL;
 
     return 0;
 }
 
+int cxBufferEnableFlushing(
+    CxBuffer *buffer,
+    CxBufferFlushConfig config
+) {
+    buffer->flush = malloc(sizeof(CxBufferFlushConfig));
+    if (buffer->flush == NULL) return -1; // LCOV_EXCL_LINE
+    memcpy(buffer->flush, &config, sizeof(CxBufferFlushConfig));
+    return 0;
+}
+
 void cxBufferDestroy(CxBuffer *buffer) {
-    if ((buffer->flags & CX_BUFFER_FREE_CONTENTS) == CX_BUFFER_FREE_CONTENTS) {
+    if (buffer->flags & CX_BUFFER_FREE_CONTENTS) {
         cxFree(buffer->allocator, buffer->bytes);
     }
+    free(buffer->flush);
+    memset(buffer, 0, sizeof(CxBuffer));
 }
 
 CxBuffer *cxBufferCreate(
@@ -76,21 +100,26 @@
         const CxAllocator *allocator,
         int flags
 ) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
+    }
     CxBuffer *buf = cxMalloc(allocator, sizeof(CxBuffer));
     if (buf == NULL) return NULL;
     if (0 == cxBufferInit(buf, space, capacity, allocator, flags)) {
         return buf;
     } else {
+        // LCOV_EXCL_START
         cxFree(allocator, buf);
         return NULL;
+        // LCOV_EXCL_STOP
     }
 }
 
 void cxBufferFree(CxBuffer *buffer) {
-    if ((buffer->flags & CX_BUFFER_FREE_CONTENTS) == CX_BUFFER_FREE_CONTENTS) {
-        cxFree(buffer->allocator, buffer->bytes);
-    }
-    cxFree(buffer->allocator, buffer);
+    if (buffer == NULL) return;
+    const CxAllocator *allocator = buffer->allocator;
+    cxBufferDestroy(buffer);
+    cxFree(allocator, buffer);
 }
 
 int cxBufferSeek(
@@ -117,10 +146,11 @@
     npos += offset;
 
     if ((offset > 0 && npos < opos) || (offset < 0 && npos > opos)) {
+        errno = EOVERFLOW;
         return -1;
     }
 
-    if (npos >= buffer->size) {
+    if (npos > buffer->size) {
         return -1;
     } else {
         buffer->pos = npos;
@@ -130,7 +160,9 @@
 }
 
 void cxBufferClear(CxBuffer *buffer) {
-    memset(buffer->bytes, 0, buffer->size);
+    if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) {
+        memset(buffer->bytes, 0, buffer->size);
+    }
     buffer->size = 0;
     buffer->pos = 0;
 }
@@ -140,7 +172,7 @@
     buffer->pos = 0;
 }
 
-int cxBufferEof(const CxBuffer *buffer) {
+bool cxBufferEof(const CxBuffer *buffer) {
     return buffer->pos >= buffer->size;
 }
 
@@ -152,48 +184,71 @@
         return 0;
     }
 
-    if (cxReallocate(buffer->allocator,
+    const int force_copy_flags = CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_COPY_ON_EXTEND;
+    if (buffer->flags & force_copy_flags) {
+        void *newspace = cxMalloc(buffer->allocator, newcap);
+        if (NULL == newspace) return -1;
+        memcpy(newspace, buffer->space, buffer->size);
+        buffer->space = newspace;
+        buffer->capacity = newcap;
+        buffer->flags &= ~force_copy_flags;
+        buffer->flags |= CX_BUFFER_FREE_CONTENTS;
+        return 0;
+    } else if (cxReallocate(buffer->allocator,
                      (void **) &buffer->bytes, newcap) == 0) {
         buffer->capacity = newcap;
         return 0;
     } else {
-        return -1;
+        return -1; // LCOV_EXCL_LINE
     }
 }
 
-/**
- * Helps flushing data to the flush target of a buffer.
- *
- * @param buffer the buffer containing the config
- * @param space the data to flush
- * @param size the element size
- * @param nitems the number of items
- * @return the number of items flushed
- */
-static size_t cx_buffer_write_flush_helper(
-        CxBuffer *buffer,
-        const unsigned char *space,
+static size_t cx_buffer_flush_helper(
+        const CxBuffer *buffer,
         size_t size,
+        const unsigned char *src,
         size_t nitems
 ) {
-    size_t pos = 0;
-    size_t remaining = nitems;
-    size_t max_items = buffer->flush_blksize / size;
-    while (remaining > 0) {
-        size_t items = remaining > max_items ? max_items : remaining;
-        size_t flushed = buffer->flush_func(
-                space + pos,
-                size, items,
-                buffer->flush_target);
+    // flush data from an arbitrary source
+    // does not need to be the buffer's contents
+    size_t max_items = buffer->flush->blksize / size;
+    size_t fblocks = 0;
+    size_t flushed_total = 0;
+    while (nitems > 0 && fblocks < buffer->flush->blkmax) {
+        fblocks++;
+        size_t items = nitems > max_items ? max_items : nitems;
+        size_t flushed = buffer->flush->wfunc(
+            src, size, items, buffer->flush->target);
         if (flushed > 0) {
-            pos += (flushed * size);
-            remaining -= flushed;
+            flushed_total += flushed;
+            src += flushed * size;
+            nitems -= flushed;
         } else {
             // if no bytes can be flushed out anymore, we give up
             break;
         }
     }
-    return nitems - remaining;
+    return flushed_total;
+}
+
+static size_t cx_buffer_flush_impl(CxBuffer *buffer, size_t size) {
+    // flush the current contents of the buffer
+    unsigned char *space = buffer->bytes;
+    size_t remaining = buffer->pos / size;
+    size_t flushed_total = cx_buffer_flush_helper(
+        buffer, size, space, remaining);
+
+    // shift the buffer left after flushing
+    // IMPORTANT: up to this point, copy on write must have been
+    // performed already, because we can't do error handling here
+    cxBufferShiftLeft(buffer, flushed_total*size);
+
+    return flushed_total;
+}
+
+size_t cxBufferFlush(CxBuffer *buffer) {
+    if (buffer_copy_on_write(buffer)) return 0;
+    return cx_buffer_flush_impl(buffer, 1);
 }
 
 size_t cxBufferWrite(
@@ -204,6 +259,7 @@
 ) {
     // optimize for easy case
     if (size == 1 && (buffer->capacity - buffer->pos) >= nitems) {
+        if (buffer_copy_on_write(buffer)) return 0;
         memcpy(buffer->bytes + buffer->pos, ptr, nitems);
         buffer->pos += nitems;
         if (buffer->pos > buffer->size) {
@@ -213,80 +269,69 @@
     }
 
     size_t len;
-    size_t nitems_out = nitems;
     if (cx_szmul(size, nitems, &len)) {
+        errno = EOVERFLOW;
+        return 0;
+    }
+    if (buffer->pos > SIZE_MAX - len) {
+        errno = EOVERFLOW;
         return 0;
     }
     size_t required = buffer->pos + len;
-    if (buffer->pos > required) {
-        return 0;
-    }
 
     bool perform_flush = false;
     if (required > buffer->capacity) {
-        if ((buffer->flags & CX_BUFFER_AUTO_EXTEND) == CX_BUFFER_AUTO_EXTEND && required) {
-            if (buffer->flush_blkmax > 0 && required > buffer->flush_threshold) {
+        if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
+            if (buffer->flush != NULL && required > buffer->flush->threshold) {
                 perform_flush = true;
             } else {
                 if (cxBufferMinimumCapacity(buffer, required)) {
-                    return 0;
+                    return 0; // LCOV_EXCL_LINE
                 }
             }
         } else {
-            if (buffer->flush_blkmax > 0) {
+            if (buffer->flush != NULL) {
                 perform_flush = true;
             } else {
-                // truncate data to be written, if we can neither extend nor flush
+                // truncate data, if we can neither extend nor flush
                 len = buffer->capacity - buffer->pos;
                 if (size > 1) {
                     len -= len % size;
                 }
-                nitems_out = len / size;
+                nitems = len / size;
             }
         }
     }
 
+    // check here and not above because of possible truncation
     if (len == 0) {
-        return len;
+        return 0;
     }
 
-    if (perform_flush) {
-        size_t flush_max;
-        if (cx_szmul(buffer->flush_blkmax, buffer->flush_blksize, &flush_max)) {
-            return 0;
-        }
-        size_t flush_pos = buffer->flush_func == NULL || buffer->flush_target == NULL
-                           ? buffer->pos
-                           : cx_buffer_write_flush_helper(buffer, buffer->bytes, 1, buffer->pos);
-        if (flush_pos == buffer->pos) {
-            // entire buffer has been flushed, we can reset
-            buffer->size = buffer->pos = 0;
-
-            size_t items_flush; // how many items can also be directly flushed
-            size_t items_keep; // how many items have to be written to the buffer
+    // check if we need to copy
+    if (buffer_copy_on_write(buffer)) return 0;
 
-            items_flush = flush_max >= required ? nitems : (flush_max - flush_pos) / size;
-            if (items_flush > 0) {
-                items_flush = cx_buffer_write_flush_helper(buffer, ptr, size, items_flush / size);
-                // in case we could not flush everything, keep the rest
+    // perform the operation
+    if (perform_flush) {
+        size_t items_flush;
+        if (buffer->pos == 0) {
+            // if we don't have data in the buffer, but are instructed
+            // to flush, it means that we are supposed to relay the data
+            items_flush = cx_buffer_flush_helper(buffer, size, ptr, nitems);
+            if (items_flush == 0) {
+                // we needed to flush, but could not flush anything
+                // give up and avoid endless trying
+                return 0;
             }
-            items_keep = nitems - items_flush;
-            if (items_keep > 0) {
-                // try again with the remaining stuff
-                const unsigned char *new_ptr = ptr;
-                new_ptr += items_flush * size;
-                // report the directly flushed items as written plus the remaining stuff
-                return items_flush + cxBufferWrite(new_ptr, size, items_keep, buffer);
-            } else {
-                // all items have been flushed - report them as written
-                return nitems;
+            size_t ritems = nitems - items_flush;
+            const unsigned char *rest = ptr;
+            rest += items_flush * size;
+            return items_flush + cxBufferWrite(rest, size, ritems, buffer);
+        } else {
+            items_flush = cx_buffer_flush_impl(buffer, size);
+            if (items_flush == 0) {
+                return 0;
             }
-        } else if (flush_pos == 0) {
-            // nothing could be flushed at all, we immediately give up without writing any data
-            return 0;
-        } else {
-            // we were partially successful, we shift left and try again
-            cxBufferShiftLeft(buffer, flush_pos);
             return cxBufferWrite(ptr, size, nitems, buffer);
         }
     } else {
@@ -295,11 +340,24 @@
         if (buffer->pos > buffer->size) {
             buffer->size = buffer->pos;
         }
-        return nitems_out;
+        return nitems;
     }
 
 }
 
+size_t cxBufferAppend(
+        const void *ptr,
+        size_t size,
+        size_t nitems,
+        CxBuffer *buffer
+) {
+    size_t pos = buffer->pos;
+    buffer->pos = buffer->size;
+    size_t written = cxBufferWrite(ptr, size, nitems, buffer);
+    buffer->pos = pos;
+    return written;
+}
+
 int cxBufferPut(
         CxBuffer *buffer,
         int c
@@ -313,6 +371,17 @@
     }
 }
 
+int cxBufferTerminate(CxBuffer *buffer) {
+    bool success = 0 == cxBufferPut(buffer, 0);
+    if (success) {
+        buffer->pos--;
+        buffer->size--;
+        return 0;
+    } else {
+        return -1;
+    }
+}
+
 size_t cxBufferPutString(
         CxBuffer *buffer,
         const char *str
@@ -328,6 +397,7 @@
 ) {
     size_t len;
     if (cx_szmul(size, nitems, &len)) {
+        errno = EOVERFLOW;
         return 0;
     }
     if (buffer->pos + len > buffer->size) {
@@ -362,6 +432,7 @@
     if (shift >= buffer->size) {
         buffer->pos = buffer->size = 0;
     } else {
+        if (buffer_copy_on_write(buffer)) return -1;
         memmove(buffer->bytes, buffer->bytes + shift, buffer->size - shift);
         buffer->size -= shift;
 
@@ -378,14 +449,18 @@
         CxBuffer *buffer,
         size_t shift
 ) {
+    if (buffer->size > SIZE_MAX - shift) {
+        errno = EOVERFLOW;
+        return -1;
+    }
     size_t req_capacity = buffer->size + shift;
     size_t movebytes;
 
     // auto extend buffer, if required and enabled
     if (buffer->capacity < req_capacity) {
-        if ((buffer->flags & CX_BUFFER_AUTO_EXTEND) == CX_BUFFER_AUTO_EXTEND) {
+        if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
             if (cxBufferMinimumCapacity(buffer, req_capacity)) {
-                return 1;
+                return -1; // LCOV_EXCL_LINE
             }
             movebytes = buffer->size;
         } else {
@@ -395,8 +470,11 @@
         movebytes = buffer->size;
     }
 
-    memmove(buffer->bytes + shift, buffer->bytes, movebytes);
-    buffer->size = shift + movebytes;
+    if (movebytes > 0) {
+        if (buffer_copy_on_write(buffer)) return -1;
+        memmove(buffer->bytes + shift, buffer->bytes, movebytes);
+        buffer->size = shift + movebytes;
+    }
 
     buffer->pos += shift;
     if (buffer->pos > buffer->size) {
--- a/ucx/compare.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/compare.c	Sun Jan 05 22:00:39 2025 +0100
@@ -30,9 +30,21 @@
 
 #include <math.h>
 
+int cx_vcmp_int(int a, int b) {
+    if (a == b) {
+        return 0;
+    } else {
+        return a < b ? -1 : 1;
+    }
+}
+
 int cx_cmp_int(const void *i1, const void *i2) {
     int a = *((const int *) i1);
     int b = *((const int *) i2);
+    return cx_vcmp_int(a, b);
+}
+
+int cx_vcmp_longint(long int a, long int b) {
     if (a == b) {
         return 0;
     } else {
@@ -43,6 +55,10 @@
 int cx_cmp_longint(const void *i1, const void *i2) {
     long int a = *((const long int *) i1);
     long int b = *((const long int *) i2);
+    return cx_vcmp_longint(a, b);
+}
+
+int cx_vcmp_longlong(long long a, long long b) {
     if (a == b) {
         return 0;
     } else {
@@ -53,6 +69,10 @@
 int cx_cmp_longlong(const void *i1, const void *i2) {
     long long a = *((const long long *) i1);
     long long b = *((const long long *) i2);
+    return cx_vcmp_longlong(a, b);
+}
+
+int cx_vcmp_int16(int16_t a, int16_t b) {
     if (a == b) {
         return 0;
     } else {
@@ -63,6 +83,10 @@
 int cx_cmp_int16(const void *i1, const void *i2) {
     int16_t a = *((const int16_t *) i1);
     int16_t b = *((const int16_t *) i2);
+    return cx_vcmp_int16(a, b);
+}
+
+int cx_vcmp_int32(int32_t a, int32_t b) {
     if (a == b) {
         return 0;
     } else {
@@ -73,6 +97,10 @@
 int cx_cmp_int32(const void *i1, const void *i2) {
     int32_t a = *((const int32_t *) i1);
     int32_t b = *((const int32_t *) i2);
+    return cx_vcmp_int32(a, b);
+}
+
+int cx_vcmp_int64(int64_t a, int64_t b) {
     if (a == b) {
         return 0;
     } else {
@@ -83,6 +111,10 @@
 int cx_cmp_int64(const void *i1, const void *i2) {
     int64_t a = *((const int64_t *) i1);
     int64_t b = *((const int64_t *) i2);
+    return cx_vcmp_int64(a, b);
+}
+
+int cx_vcmp_uint(unsigned int a, unsigned int b) {
     if (a == b) {
         return 0;
     } else {
@@ -93,6 +125,10 @@
 int cx_cmp_uint(const void *i1, const void *i2) {
     unsigned int a = *((const unsigned int *) i1);
     unsigned int b = *((const unsigned int *) i2);
+    return cx_vcmp_uint(a, b);
+}
+
+int cx_vcmp_ulongint(unsigned long int a, unsigned long int b) {
     if (a == b) {
         return 0;
     } else {
@@ -103,6 +139,10 @@
 int cx_cmp_ulongint(const void *i1, const void *i2) {
     unsigned long int a = *((const unsigned long int *) i1);
     unsigned long int b = *((const unsigned long int *) i2);
+    return cx_vcmp_ulongint(a, b);
+}
+
+int cx_vcmp_ulonglong(unsigned long long a, unsigned long long b) {
     if (a == b) {
         return 0;
     } else {
@@ -113,6 +153,10 @@
 int cx_cmp_ulonglong(const void *i1, const void *i2) {
     unsigned long long a = *((const unsigned long long *) i1);
     unsigned long long b = *((const unsigned long long *) i2);
+    return cx_vcmp_ulonglong(a, b);
+}
+
+int cx_vcmp_uint16(uint16_t a, uint16_t b) {
     if (a == b) {
         return 0;
     } else {
@@ -123,6 +167,10 @@
 int cx_cmp_uint16(const void *i1, const void *i2) {
     uint16_t a = *((const uint16_t *) i1);
     uint16_t b = *((const uint16_t *) i2);
+    return cx_vcmp_uint16(a, b);
+}
+
+int cx_vcmp_uint32(uint32_t a, uint32_t b) {
     if (a == b) {
         return 0;
     } else {
@@ -133,6 +181,10 @@
 int cx_cmp_uint32(const void *i1, const void *i2) {
     uint32_t a = *((const uint32_t *) i1);
     uint32_t b = *((const uint32_t *) i2);
+    return cx_vcmp_uint32(a, b);
+}
+
+int cx_vcmp_uint64(uint64_t a, uint64_t b) {
     if (a == b) {
         return 0;
     } else {
@@ -143,7 +195,11 @@
 int cx_cmp_uint64(const void *i1, const void *i2) {
     uint64_t a = *((const uint64_t *) i1);
     uint64_t b = *((const uint64_t *) i2);
-    if (a == b) {
+    return cx_vcmp_uint64(a, b);
+}
+
+int cx_vcmp_float(float a, float b) {
+    if (fabsf(a - b) < 1e-6f) {
         return 0;
     } else {
         return a < b ? -1 : 1;
@@ -153,7 +209,11 @@
 int cx_cmp_float(const void *f1, const void *f2) {
     float a = *((const float *) f1);
     float b = *((const float *) f2);
-    if (fabsf(a - b) < 1e-6f) {
+    return cx_vcmp_float(a, b);
+}
+
+int cx_vcmp_double(double a, double b) {
+    if (fabs(a - b) < 1e-14) {
         return 0;
     } else {
         return a < b ? -1 : 1;
@@ -166,10 +226,14 @@
 ) {
     double a = *((const double *) d1);
     double b = *((const double *) d2);
-    if (fabs(a - b) < 1e-14) {
+    return cx_vcmp_double(a, b);
+}
+
+int cx_vcmp_intptr(intptr_t p1, intptr_t p2) {
+    if (p1 == p2) {
         return 0;
     } else {
-        return a < b ? -1 : 1;
+        return p1 < p2 ? -1 : 1;
     }
 }
 
@@ -179,6 +243,10 @@
 ) {
     intptr_t p1 = *(const intptr_t *) ptr1;
     intptr_t p2 = *(const intptr_t *) ptr2;
+    return cx_vcmp_intptr(p1, p2);
+}
+
+int cx_vcmp_uintptr(uintptr_t p1, uintptr_t p2) {
     if (p1 == p2) {
         return 0;
     } else {
@@ -192,11 +260,7 @@
 ) {
     uintptr_t p1 = *(const uintptr_t *) ptr1;
     uintptr_t p2 = *(const uintptr_t *) ptr2;
-    if (p1 == p2) {
-        return 0;
-    } else {
-        return p1 < p2 ? -1 : 1;
-    }
+    return cx_vcmp_uintptr(p1, p2);
 }
 
 int cx_cmp_ptr(
--- a/ucx/cx/allocator.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/allocator.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,7 +26,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file allocator.h
+ * @file allocator.h
  * Interface for custom allocators.
  */
 
@@ -54,7 +54,6 @@
     /**
      * The allocator's realloc() implementation.
      */
-    __attribute__((__warn_unused_result__))
     void *(*realloc)(
             void *data,
             void *mem,
@@ -73,7 +72,6 @@
     /**
      * The allocator's free() implementation.
      */
-    __attribute__((__nonnull__))
     void (*free)(
             void *data,
             void *mem
@@ -108,76 +106,157 @@
  * Function pointer type for destructor functions.
  *
  * A destructor function deallocates possible contents and MAY free the memory
- * pointed to by \p memory. Read the documentation of the respective function
- * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in that
- * particular implementation.
+ * pointed to by @p memory. Read the documentation of the respective function
+ * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in
+ * that particular implementation.
  *
  * @param memory a pointer to the object to destruct
   */
-__attribute__((__nonnull__))
 typedef void (*cx_destructor_func)(void *memory);
 
 /**
  * Function pointer type for destructor functions.
  *
  * A destructor function deallocates possible contents and MAY free the memory
- * pointed to by \p memory. Read the documentation of the respective function
- * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in that
- * particular implementation.
+ * pointed to by @p memory. Read the documentation of the respective function
+ * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in
+ * that particular implementation.
  *
  * @param data an optional pointer to custom data
  * @param memory a pointer to the object to destruct
   */
-__attribute__((__nonnull__(2)))
 typedef void (*cx_destructor_func2)(
         void *data,
         void *memory
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
  *
- * \par Error handling
- * \c errno will be set by realloc() on failure.
+ * @par Error handling
+ * @c errno will be set by realloc() on failure.
  *
  * @param mem pointer to the pointer to allocated block
  * @param n the new size in bytes
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_reallocatearray()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_reallocate(
         void **mem,
         size_t n
 );
 
 /**
- * Allocate \p n bytes of memory.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ *
+ * The size is calculated by multiplying @p nemb and @p size.
+ *
+ * @par Error handling
+ * @c errno will be set by realloc() on failure or when the multiplication of
+ * @p nmemb and @p size overflows.
+ *
+ * @param mem pointer to the pointer to allocated block
+ * @param nmemb the number of elements
+ * @param size the size of each element
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_reallocate()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_reallocatearray(
+        void **mem,
+        size_t nmemb,
+        size_t size
+);
+
+/**
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ *
+ * @par Error handling
+ * @c errno will be set by realloc() on failure.
+ *
+ * @param mem (@c void**) pointer to the pointer to allocated block
+ * @param n (@c size_t) the new size in bytes
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_reallocatearray()
+ */
+#define cx_reallocate(mem, n) cx_reallocate((void**)(mem), n)
+
+/**
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ *
+ * The size is calculated by multiplying @p nemb and @p size.
+ *
+ * @par Error handling
+ * @c errno will be set by realloc() on failure or when the multiplication of
+ * @p nmemb and @p size overflows.
+ *
+ * @param mem (@c void**) pointer to the pointer to allocated block
+ * @param nmemb (@c size_t) the number of elements
+ * @param size (@c size_t) the size of each element
+ * @retval zero success
+ * @retval non-zero failure
+ */
+#define cx_reallocatearray(mem, nmemb, size) \
+    cx_reallocatearray((void**)(mem), nmemb, size)
+
+/**
+ * Free a block allocated by this allocator.
+ *
+ * @note Freeing a block of a different allocator is undefined.
+ *
+ * @param allocator the allocator
+ * @param mem a pointer to the block to free
+ */
+cx_attr_nonnull_arg(1)
+void cxFree(
+        const CxAllocator *allocator,
+        void *mem
+);
+
+/**
+ * Allocate @p n bytes of memory.
  *
  * @param allocator the allocator
  * @param n the number of bytes
  * @return a pointer to the allocated memory
  */
-__attribute__((__malloc__))
-__attribute__((__alloc_size__(2)))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_malloc
+cx_attr_dealloc_ucx
+cx_attr_allocsize(2)
 void *cxMalloc(
         const CxAllocator *allocator,
         size_t n
 );
 
 /**
- * Re-allocate the previously allocated block in \p mem, making the new block \p n bytes long.
- * This function may return the same pointer that was passed to it, if moving the memory
- * was not necessary.
+ * Re-allocate the previously allocated block in @p mem, making the new block
+ * @p n bytes long.
+ * This function may return the same pointer that was passed to it, if moving
+ * the memory was not necessary.
  *
- * \note Re-allocating a block allocated by a different allocator is undefined.
+ * @note Re-allocating a block allocated by a different allocator is undefined.
  *
  * @param allocator the allocator
  * @param mem pointer to the previously allocated block
  * @param n the new size in bytes
  * @return a pointer to the re-allocated memory
  */
-__attribute__((__warn_unused_result__))
-__attribute__((__alloc_size__(3)))
+cx_attr_nodiscard
+cx_attr_nonnull_arg(1)
+cx_attr_dealloc_ucx
+cx_attr_allocsize(3)
 void *cxRealloc(
         const CxAllocator *allocator,
         void *mem,
@@ -185,20 +264,52 @@
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
- * This function acts like cxRealloc() using the pointer pointed to by \p mem.
+ * Re-allocate the previously allocated block in @p mem, making the new block
+ * @p n bytes long.
+ * This function may return the same pointer that was passed to it, if moving
+ * the memory was not necessary.
+ *
+ * The size is calculated by multiplying @p nemb and @p size.
+ * If that multiplication overflows, this function returns @c NULL and @c errno
+ * will be set.
+ *
+ * @note Re-allocating a block allocated by a different allocator is undefined.
  *
- * \note Re-allocating a block allocated by a different allocator is undefined.
+ * @param allocator the allocator
+ * @param mem pointer to the previously allocated block
+ * @param nmemb the number of elements
+ * @param size the size of each element
+ * @return a pointer to the re-allocated memory
+ */
+cx_attr_nodiscard
+cx_attr_nonnull_arg(1)
+cx_attr_dealloc_ucx
+cx_attr_allocsize(3, 4)
+void *cxReallocArray(
+        const CxAllocator *allocator,
+        void *mem,
+        size_t nmemb,
+        size_t size
+);
+
+/**
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ * This function acts like cxRealloc() using the pointer pointed to by @p mem.
  *
- * \par Error handling
- * \c errno will be set, if the underlying realloc function does so.
+ * @note Re-allocating a block allocated by a different allocator is undefined.
+ *
+ * @par Error handling
+ * @c errno will be set, if the underlying realloc function does so.
  *
  * @param allocator the allocator
  * @param mem pointer to the pointer to allocated block
  * @param n the new size in bytes
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  */
-__attribute__((__nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 int cxReallocate(
         const CxAllocator *allocator,
         void **mem,
@@ -206,35 +317,93 @@
 );
 
 /**
- * Allocate \p nelem elements of \p n bytes each, all initialized to zero.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ * This function acts like cxRealloc() using the pointer pointed to by @p mem.
+ *
+ * @note Re-allocating a block allocated by a different allocator is undefined.
+ *
+ * @par Error handling
+ * @c errno will be set, if the underlying realloc function does so.
+ *
+ * @param allocator (@c CxAllocator*) the allocator
+ * @param mem (@c void**) pointer to the pointer to allocated block
+ * @param n (@c size_t) the new size in bytes
+ * @retval zero success
+ * @retval non-zero failure
+ */
+#define cxReallocate(allocator, mem, n) \
+    cxReallocate(allocator, (void**)(mem), n)
+
+/**
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ * This function acts like cxReallocArray() using the pointer pointed to
+ * by @p mem.
+ *
+ * @note Re-allocating a block allocated by a different allocator is undefined.
+ *
+ * @par Error handling
+ * @c errno will be set, if the underlying realloc function does so or the
+ * multiplication of @p nmemb and @p size overflows.
+ *
+ * @param allocator the allocator
+ * @param mem pointer to the pointer to allocated block
+ * @param nmemb the number of elements
+ * @param size the size of each element
+ * @retval zero success
+ * @retval non-zero on failure
+ */
+cx_attr_nodiscard
+cx_attr_nonnull
+int cxReallocateArray(
+        const CxAllocator *allocator,
+        void **mem,
+        size_t nmemb,
+        size_t size
+);
+
+/**
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ * This function acts like cxReallocArray() using the pointer pointed to
+ * by @p mem.
+ *
+ * @note Re-allocating a block allocated by a different allocator is undefined.
+ *
+ * @par Error handling
+ * @c errno will be set, if the underlying realloc function does so or the
+ * multiplication of @p nmemb and @p size overflows.
+ *
+ * @param allocator (@c CxAllocator*) the allocator
+ * @param mem (@c void**) pointer to the pointer to allocated block
+ * @param nmemb (@c size_t) the number of elements
+ * @param size (@c size_t) the size of each element
+ * @retval zero success
+ * @retval non-zero failure
+ */
+#define cxReallocateArray(allocator, mem, nmemb, size) \
+        cxReallocateArray(allocator, (void**) (mem), nmemb, size)
+
+/**
+ * Allocate @p nelem elements of @p n bytes each, all initialized to zero.
  *
  * @param allocator the allocator
  * @param nelem the number of elements
  * @param n the size of each element in bytes
  * @return a pointer to the allocated memory
  */
-__attribute__((__malloc__))
-__attribute__((__alloc_size__(2, 3)))
+cx_attr_nonnull_arg(1)
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc_ucx
+cx_attr_allocsize(2, 3)
 void *cxCalloc(
         const CxAllocator *allocator,
         size_t nelem,
         size_t n
 );
 
-/**
- * Free a block allocated by this allocator.
- *
- * \note Freeing a block of a different allocator is undefined.
- *
- * @param allocator the allocator
- * @param mem a pointer to the block to free
- */
-__attribute__((__nonnull__))
-void cxFree(
-        const CxAllocator *allocator,
-        void *mem
-);
-
 #ifdef __cplusplus
 } // extern "C"
 #endif
--- a/ucx/cx/array_list.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/array_list.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,12 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file array_list.h
- * \brief Array list implementation.
- * \details Also provides several low-level functions for custom array list implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file array_list.h
+ * @brief Array list implementation.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 
@@ -45,30 +44,90 @@
 #endif
 
 /**
- * The maximum item size in an array list that fits into stack buffer when swapped.
+ * The maximum item size in an array list that fits into stack buffer
+ * when swapped.
  */
-extern unsigned cx_array_swap_sbo_size;
+extern const unsigned cx_array_swap_sbo_size;
+
+/**
+ * Declares variables for an array that can be used with the convenience macros.
+ *
+ * @par Examples
+ * @code
+ * // integer array with at most 255 elements
+ * CX_ARRAY_DECLARE_SIZED(int, myarray, uint8_t)
+ *
+ * // array of MyObject* pointers where size and capacity are stored as unsigned int
+ * CX_ARRAY_DECLARE_SIZED(MyObject*, objects, unsigned int)
+ *
+ * // initializing code
+ * cx_array_initialize(myarray, 16); // reserve space for 16
+ * cx_array_initialize(objects, 100); // reserve space for 100
+ * @endcode
+ *
+ * @param type the type of the data
+ * @param name the name of the array
+ * @param size_type the type of the size (should be uint8_t, uint16_t, uint32_t, or size_t)
+ *
+ * @see cx_array_initialize()
+ * @see cx_array_simple_add()
+ * @see cx_array_simple_copy()
+ * @see cx_array_simple_add_sorted()
+ * @see cx_array_simple_insert_sorted()
+ */
+#define CX_ARRAY_DECLARE_SIZED(type, name, size_type) \
+    type * name; \
+    /** Array size. */ size_type name##_size; \
+    /** Array capacity. */ size_type name##_capacity
 
 /**
  * Declares variables for an array that can be used with the convenience macros.
  *
+ * The size and capacity variables will have @c size_t type.
+ * Use #CX_ARRAY_DECLARE_SIZED() to specify a different type.
+ *
+ * @par Examples
+ * @code
+ * // int array
+ * CX_ARRAY_DECLARE(int, myarray)
+ *
+ * // initializing code
+ * cx_array_initialize(myarray, 32); // reserve space for 32
+ * @endcode
+ *
+ * @param type the type of the data
+ * @param name the name of the array
+ *
+ * @see cx_array_initialize()
  * @see cx_array_simple_add()
  * @see cx_array_simple_copy()
- * @see cx_array_initialize()
  * @see cx_array_simple_add_sorted()
  * @see cx_array_simple_insert_sorted()
  */
-#define CX_ARRAY_DECLARE(type, name) \
-    type * name;                     \
-    size_t name##_size;              \
-    size_t name##_capacity
+#define CX_ARRAY_DECLARE(type, name) CX_ARRAY_DECLARE_SIZED(type, name, size_t)
 
 /**
- * Initializes an array declared with CX_ARRAY_DECLARE().
+ * Initializes an array with the given capacity.
+ *
+ * The type of the capacity depends on the type used during declaration.
+ *
+ * @par Examples
+ * @code
+ * CX_ARRAY_DECLARE_SIZED(int, arr1, uint8_t)
+ * CX_ARRAY_DECLARE(int, arr2) // size and capacity are implicitly size_t
+ *
+ * // initializing code
+ * cx_array_initialize(arr1, 500); // error: maximum for uint8_t is 255
+ * cx_array_initialize(arr2, 500); // OK
+ * @endcode
+ *
  *
  * The memory for the array is allocated with stdlib malloc().
- * @param array the array
+ * @param array the name of the array
  * @param capacity the initial capacity
+ * @see cx_array_initialize_a()
+ * @see CX_ARRAY_DECLARE_SIZED()
+ * @see CX_ARRAY_DECLARE()
  */
 #define cx_array_initialize(array, capacity) \
         array##_capacity = capacity; \
@@ -76,7 +135,36 @@
         array = malloc(sizeof(array[0]) * capacity)
 
 /**
+ * Initializes an array with the given capacity using the specified allocator.
+ *
+ * @par Example
+ * @code
+ * CX_ARRAY_DECLARE(int, myarray)
+ *
+ *
+ * const CxAllocator *al = // ...
+ * cx_array_initialize_a(al, myarray, 128);
+ * // ...
+ * cxFree(al, myarray); // don't forget to free with same allocator
+ * @endcode
+ *
+ * The memory for the array is allocated with stdlib malloc().
+ * @param allocator (@c CxAllocator*) the allocator
+ * @param array the name of the array
+ * @param capacity the initial capacity
+ * @see cx_array_initialize()
+ * @see CX_ARRAY_DECLARE_SIZED()
+ * @see CX_ARRAY_DECLARE()
+ */
+#define cx_array_initialize_a(allocator, array, capacity) \
+        array##_capacity = capacity; \
+        array##_size = 0; \
+        array = cxMalloc(allocator, sizeof(array[0]) * capacity)
+
+/**
  * Defines a reallocation mechanism for arrays.
+ * You can create your own, use cx_array_reallocator(), or
+ * use the #cx_array_default_reallocator.
  */
 struct cx_array_reallocator_s {
     /**
@@ -92,8 +180,11 @@
      * @param capacity the new capacity (number of elements)
      * @param elem_size the size of each element
      * @param alloc a reference to this allocator
-     * @return a pointer to the reallocated memory or \c NULL on failure
+     * @return a pointer to the reallocated memory or @c NULL on failure
      */
+    cx_attr_nodiscard
+    cx_attr_nonnull_arg(4)
+    cx_attr_allocsize(2, 3)
     void *(*realloc)(
             void *array,
             size_t capacity,
@@ -120,125 +211,271 @@
 };
 
 /**
+ * Typedef for the array reallocator struct.
+ */
+typedef struct cx_array_reallocator_s CxArrayReallocator;
+
+/**
  * A default stdlib-based array reallocator.
  */
-extern struct cx_array_reallocator_s *cx_array_default_reallocator;
+extern CxArrayReallocator *cx_array_default_reallocator;
+
+/**
+ * Creates a new array reallocator.
+ *
+ * When @p allocator is @c NULL, the stdlib default allocator will be used.
+ *
+ * When @p stackmem is not @c NULL, the reallocator is supposed to be used
+ * @em only for the specific array that is initially located at @p stackmem.
+ * When reallocation is needed, the reallocator checks, if the array is
+ * still located at @p stackmem and copies the contents to the heap.
+ *
+ * @note Invoking this function with both arguments @c NULL will return a
+ * reallocator that behaves like #cx_array_default_reallocator.
+ *
+ * @param allocator the allocator this reallocator shall be based on
+ * @param stackmem the address of the array when the array is initially located
+ * on the stack or shall not reallocated in place
+ * @return an array reallocator
+ */
+CxArrayReallocator cx_array_reallocator(
+        const struct cx_allocator_s *allocator,
+        const void *stackmem
+);
 
 /**
- * Return codes for array functions.
+ * Reserves memory for additional elements.
+ *
+ * This function checks if the @p capacity of the array is sufficient to hold
+ * at least @p size plus @p elem_count elements. If not, a reallocation is
+ * performed with the specified @p reallocator.
+ * You can create your own reallocator by hand, use #cx_array_default_reallocator,
+ * or use the convenience function cx_array_reallocator() to create a custom reallocator.
+ *
+ * This function can be useful to replace subsequent calls to cx_array_copy()
+ * with one single cx_array_reserve() and then - after guaranteeing a
+ * sufficient capacity - use simple memmove() or memcpy().
+ *
+ * The @p width in bytes refers to the size and capacity.
+ * Both must have the same width.
+ * Supported are 0, 1, 2, and 4, as well as 8 if running on a 64 bit
+ * architecture. If set to zero, the native word width is used.
+ *
+ * @param array a pointer to the target array
+ * @param size a pointer to the size of the array
+ * @param capacity a pointer to the capacity of the array
+ * @param width the width in bytes for the @p size and @p capacity or zero for default
+ * @param elem_size the size of one element
+ * @param elem_count the number of expected additional elements
+ * @param reallocator the array reallocator to use
+ * (@c NULL defaults to #cx_array_default_reallocator)
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_array_reallocator()
  */
-enum cx_array_result {
-    CX_ARRAY_SUCCESS,
-    CX_ARRAY_REALLOC_NOT_SUPPORTED,
-    CX_ARRAY_REALLOC_FAILED,
-};
+cx_attr_nonnull_arg(1, 2, 3)
+int cx_array_reserve(
+        void **array,
+        void *size,
+        void *capacity,
+        unsigned width,
+        size_t elem_size,
+        size_t elem_count,
+        CxArrayReallocator *reallocator
+);
 
 /**
  * Copies elements from one array to another.
  *
- * The elements are copied to the \p target array at the specified \p index,
- * overwriting possible elements. The \p index does not need to be in range of
- * the current array \p size. If the new index plus the number of elements added
- * would extend the array's size, and \p capacity is not \c NULL, the remaining
- * capacity is used.
+ * The elements are copied to the @p target array at the specified @p index,
+ * overwriting possible elements. The @p index does not need to be in range of
+ * the current array @p size. If the new index plus the number of elements added
+ * would extend the array's size, the remaining @p capacity is used.
  *
- * If the capacity is insufficient to hold the new data, a reallocation
- * attempt is made, unless the \p reallocator is set to \c NULL, in which case
- * this function ultimately returns a failure.
+ * If the @p capacity is also insufficient to hold the new data, a reallocation
+ * attempt is made with the specified @p reallocator.
+ * You can create your own reallocator by hand, use #cx_array_default_reallocator,
+ * or use the convenience function cx_array_reallocator() to create a custom reallocator.
+ *
+ * The @p width in bytes refers to the size and capacity.
+ * Both must have the same width.
+ * Supported are 0, 1, 2, and 4, as well as 8 if running on a 64 bit
+ * architecture. If set to zero, the native word width is used.
  *
  * @param target a pointer to the target array
  * @param size a pointer to the size of the target array
- * @param capacity a pointer to the target array's capacity -
- * \c NULL if only the size shall be used to bound the array (reallocations
- * will NOT be supported in that case)
+ * @param capacity a pointer to the capacity of the target array
+ * @param width the width in bytes for the @p size and @p capacity or zero for default
  * @param index the index where the copied elements shall be placed
  * @param src the source array
  * @param elem_size the size of one element
  * @param elem_count the number of elements to copy
- * @param reallocator the array reallocator to use, or \c NULL
- * if reallocation shall not happen
- * @return zero on success, non-zero error code on failure
+ * @param reallocator the array reallocator to use
+ * (@c NULL defaults to #cx_array_default_reallocator)
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_array_reallocator()
  */
-__attribute__((__nonnull__(1, 2, 5)))
-enum cx_array_result cx_array_copy(
+cx_attr_nonnull_arg(1, 2, 3, 6)
+int cx_array_copy(
         void **target,
-        size_t *size,
-        size_t *capacity,
+        void *size,
+        void *capacity,
+        unsigned width,
         size_t index,
         const void *src,
         size_t elem_size,
         size_t elem_count,
-        struct cx_array_reallocator_s *reallocator
+        CxArrayReallocator *reallocator
 );
 
 /**
- * Convenience macro that uses cx_array_copy() with a default layout and the default reallocator.
+ * Convenience macro that uses cx_array_copy() with a default layout and
+ * the specified reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
- * @param index the index where the copied elements shall be placed
- * @param src the source array
- * @param count the number of elements to copy
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param index (@c size_t) the index where the copied elements shall be placed
+ * @param src (@c void*) the source array
+ * @param count (@c size_t) the number of elements to copy
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_copy()
+ */
+#define cx_array_simple_copy_a(reallocator, array, index, src, count) \
+    cx_array_copy((void**)&(array), &(array##_size), &(array##_capacity), \
+        sizeof(array##_size), index, src, sizeof((array)[0]), count, \
+        reallocator)
+
+/**
+ * Convenience macro that uses cx_array_copy() with a default layout and
+ * the default reallocator.
+ *
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param index (@c size_t) the index where the copied elements shall be placed
+ * @param src (@c void*) the source array
+ * @param count (@c size_t) the number of elements to copy
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_copy_a()
  */
 #define cx_array_simple_copy(array, index, src, count) \
-    cx_array_copy((void**)&(array), &(array##_size), &(array##_capacity), \
-    index, src, sizeof((array)[0]), count, cx_array_default_reallocator)
+    cx_array_simple_copy_a(NULL, array, index, src, count)
+
+/**
+ * Convenience macro that uses cx_array_reserve() with a default layout and
+ * the specified reallocator.
+ *
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param count (@c size_t) the number of expected @em additional elements
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_reserve()
+ */
+#define cx_array_simple_reserve_a(reallocator, array, count) \
+    cx_array_reserve((void**)&(array), &(array##_size), &(array##_capacity), \
+        sizeof(array##_size), sizeof((array)[0]), count, \
+        reallocator)
+
+/**
+ * Convenience macro that uses cx_array_reserve() with a default layout and
+ * the default reallocator.
+ *
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param count (@c size_t) the number of expected additional elements
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_reserve_a()
+ */
+#define cx_array_simple_reserve(array, count) \
+    cx_array_simple_reserve_a(NULL, array, count)
 
 /**
  * Adds an element to an array with the possibility of allocating more space.
  *
- * The element \p elem is added to the end of the \p target array which containing
- * \p size elements, already. The \p capacity must not be \c NULL and point a
- * variable holding the current maximum number of elements the array can hold.
+ * The element @p elem is added to the end of the @p target array which contains
+ * @p size elements, already. The @p capacity must point to a variable denoting
+ * the current maximum number of elements the array can hold.
  *
- * If the capacity is insufficient to hold the new element, and the optional
- * \p reallocator is not \c NULL, an attempt increase the \p capacity is made
- * and the new capacity is written back.
+ * If the capacity is insufficient to hold the new element, an attempt to
+ * increase the @p capacity is made and the new capacity is written back.
  *
- * @param target a pointer to the target array
- * @param size a pointer to the size of the target array
- * @param capacity a pointer to the target array's capacity - must not be \c NULL
- * @param elem_size the size of one element
- * @param elem a pointer to the element to add
- * @param reallocator the array reallocator to use, or \c NULL if reallocation shall not happen
- * @return zero on success, non-zero error code on failure
+ * The \@ SIZE_TYPE is flexible and can be any unsigned integer type.
+ * It is important, however, that @p size and @p capacity are pointers to
+ * variables of the same type.
+ *
+ * @param target (@c void**) a pointer to the target array
+ * @param size (@c SIZE_TYPE*) a pointer to the size of the target array
+ * @param capacity (@c SIZE_TYPE*) a pointer to the capacity of the target array
+ * @param elem_size (@c size_t) the size of one element
+ * @param elem (@c void*) a pointer to the element to add
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @retval zero success
+ * @retval non-zero failure
  */
 #define cx_array_add(target, size, capacity, elem_size, elem, reallocator) \
-    cx_array_copy((void**)(target), size, capacity, *(size), elem, elem_size, 1, reallocator)
+    cx_array_copy((void**)(target), size, capacity, sizeof(*(size)), \
+    *(size), elem, elem_size, 1, reallocator)
+
+/**
+ * Convenience macro that uses cx_array_add() with a default layout and
+ * the specified reallocator.
+ *
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param elem the element to add (NOT a pointer, address is automatically taken)
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_add()
+ */
+#define cx_array_simple_add_a(reallocator, array, elem) \
+    cx_array_simple_copy_a(reallocator, array, array##_size, &(elem), 1)
 
 /**
  * Convenience macro that uses cx_array_add() with a default layout and
  * the default reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
+ * @param array the name of the array (NOT a pointer or alias to the array)
  * @param elem the element to add (NOT a pointer, address is automatically taken)
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_add_a()
  */
 #define cx_array_simple_add(array, elem) \
-    cx_array_simple_copy(array, array##_size, &(elem), 1)
-
+    cx_array_simple_add_a(cx_array_default_reallocator, array, elem)
 
 /**
  * Inserts a sorted array into another sorted array.
  *
  * If either the target or the source array is not already sorted with respect
- * to the specified \p cmp_func, the behavior is undefined.
+ * to the specified @p cmp_func, the behavior is undefined.
  *
  * If the capacity is insufficient to hold the new data, a reallocation
  * attempt is made.
+ * You can create your own reallocator by hand, use #cx_array_default_reallocator,
+ * or use the convenience function cx_array_reallocator() to create a custom reallocator.
  *
  * @param target a pointer to the target array
  * @param size a pointer to the size of the target array
- * @param capacity a pointer to the target array's capacity
+ * @param capacity a pointer to the capacity of the target array
  * @param cmp_func the compare function for the elements
  * @param src the source array
  * @param elem_size the size of one element
  * @param elem_count the number of elements to insert
  * @param reallocator the array reallocator to use
- * @return zero on success, non-zero error code on failure
+ * (@c NULL defaults to #cx_array_default_reallocator)
+ * @retval zero success
+ * @retval non-zero failure
  */
-__attribute__((__nonnull__))
-enum cx_array_result cx_array_insert_sorted(
+cx_attr_nonnull_arg(1, 2, 3, 5)
+int cx_array_insert_sorted(
         void **target,
         size_t *size,
         size_t *capacity,
@@ -246,68 +483,112 @@
         const void *src,
         size_t elem_size,
         size_t elem_count,
-        struct cx_array_reallocator_s *reallocator
+        CxArrayReallocator *reallocator
 );
 
 /**
  * Inserts an element into a sorted array.
  *
  * If the target array is not already sorted with respect
- * to the specified \p cmp_func, the behavior is undefined.
+ * to the specified @p cmp_func, the behavior is undefined.
  *
  * If the capacity is insufficient to hold the new data, a reallocation
  * attempt is made.
  *
- * @param target a pointer to the target array
- * @param size a pointer to the size of the target array
- * @param capacity a pointer to the target array's capacity
- * @param elem_size the size of one element
- * @param elem a pointer to the element to add
- * @param reallocator the array reallocator to use
- * @return zero on success, non-zero error code on failure
+ * The \@ SIZE_TYPE is flexible and can be any unsigned integer type.
+ * It is important, however, that @p size and @p capacity are pointers to
+ * variables of the same type.
+ *
+ * @param target (@c void**) a pointer to the target array
+ * @param size (@c SIZE_TYPE*) a pointer to the size of the target array
+ * @param capacity (@c SIZE_TYPE*) a pointer to the capacity of the target array
+ * @param elem_size (@c size_t) the size of one element
+ * @param elem (@c void*) a pointer to the element to add
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @retval zero success
+ * @retval non-zero failure
  */
 #define cx_array_add_sorted(target, size, capacity, elem_size, elem, cmp_func, reallocator) \
     cx_array_insert_sorted((void**)(target), size, capacity, cmp_func, elem, elem_size, 1, reallocator)
 
 /**
  * Convenience macro for cx_array_add_sorted() with a default
+ * layout and the specified reallocator.
+ *
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param elem the element to add (NOT a pointer, address is automatically taken)
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_add_sorted()
+ */
+#define cx_array_simple_add_sorted_a(reallocator, array, elem, cmp_func) \
+    cx_array_add_sorted(&array, &(array##_size), &(array##_capacity), \
+        sizeof((array)[0]), &(elem), cmp_func, reallocator)
+
+/**
+ * Convenience macro for cx_array_add_sorted() with a default
  * layout and the default reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
+ * @param array the name of the array (NOT a pointer or alias to the array)
  * @param elem the element to add (NOT a pointer, address is automatically taken)
- * @param cmp_func the compare function for the elements
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_add_sorted_a()
  */
 #define cx_array_simple_add_sorted(array, elem, cmp_func) \
-    cx_array_add_sorted(&array, &(array##_size), &(array##_capacity), \
-        sizeof((array)[0]), &(elem), cmp_func, cx_array_default_reallocator)
+    cx_array_simple_add_sorted_a(NULL, array, elem, cmp_func)
+
+/**
+ * Convenience macro for cx_array_insert_sorted() with a default
+ * layout and the specified reallocator.
+ *
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param src (@c void*) pointer to the source array
+ * @param n (@c size_t) number of elements in the source array
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
+ * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_insert_sorted()
+ */
+#define cx_array_simple_insert_sorted_a(reallocator, array, src, n, cmp_func) \
+    cx_array_insert_sorted((void**)(&array), &(array##_size), &(array##_capacity), \
+        cmp_func, src, sizeof((array)[0]), n, reallocator)
 
 /**
  * Convenience macro for cx_array_insert_sorted() with a default
  * layout and the default reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
- * @param src pointer to the source array
- * @param n number of elements in the source array
- * @param cmp_func the compare function for the elements
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param src (@c void*) pointer to the source array
+ * @param n (@c size_t) number of elements in the source array
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
+ * @see cx_array_simple_insert_sorted_a()
  */
 #define cx_array_simple_insert_sorted(array, src, n, cmp_func) \
-    cx_array_insert_sorted((void**)(&array), &(array##_size), &(array##_capacity), \
-        cmp_func, src, sizeof((array)[0]), n, cx_array_default_reallocator)
-
+    cx_array_simple_insert_sorted_a(NULL, array, src, n, cmp_func)
 
 /**
  * Searches the largest lower bound in a sorted array.
  *
  * In other words, this function returns the index of the largest element
- * in \p arr that is less or equal to \p elem with respect to \p cmp_func.
- * When no such element exists, \p size is returned.
+ * in @p arr that is less or equal to @p elem with respect to @p cmp_func.
+ * When no such element exists, @p size is returned.
  *
- * If \p elem is contained in the array, this is identical to
+ * If @p elem is contained in the array, this is identical to
  * #cx_array_binary_search().
  *
- * If the array is not sorted with respect to the \p cmp_func, the behavior
+ * If the array is not sorted with respect to the @p cmp_func, the behavior
  * is undefined.
  *
  * @param arr the array to search
@@ -315,9 +596,11 @@
  * @param elem_size the size of one element
  * @param elem the element to find
  * @param cmp_func the compare function
- * @return the index of the largest lower bound, or \p size
+ * @return the index of the largest lower bound, or @p size
+ * @see cx_array_binary_search_sup()
+ * @see cx_array_binary_search()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cx_array_binary_search_inf(
         const void *arr,
         size_t size,
@@ -329,7 +612,7 @@
 /**
  * Searches an item in a sorted array.
  *
- * If the array is not sorted with respect to the \p cmp_func, the behavior
+ * If the array is not sorted with respect to the @p cmp_func, the behavior
  * is undefined.
  *
  * @param arr the array to search
@@ -337,38 +620,31 @@
  * @param elem_size the size of one element
  * @param elem the element to find
  * @param cmp_func the compare function
- * @return the index of the element in the array, or \p size if the element
+ * @return the index of the element in the array, or @p size if the element
  * cannot be found
+ * @see cx_array_binary_search_inf()
+ * @see cx_array_binary_search_sup()
  */
-__attribute__((__nonnull__))
-static inline size_t cx_array_binary_search(
+cx_attr_nonnull
+size_t cx_array_binary_search(
         const void *arr,
         size_t size,
         size_t elem_size,
         const void *elem,
         cx_compare_func cmp_func
-) {
-    size_t index = cx_array_binary_search_inf(
-            arr, size, elem_size, elem, cmp_func
-    );
-    if (index < size && cmp_func(((const char *) arr) + index * elem_size, elem) == 0) {
-        return index;
-    } else {
-        return size;
-    }
-}
+);
 
 /**
  * Searches the smallest upper bound in a sorted array.
  *
  * In other words, this function returns the index of the smallest element
- * in \p arr that is greater or equal to \p elem with respect to \p cmp_func.
- * When no such element exists, \p size is returned.
+ * in @p arr that is greater or equal to @p elem with respect to @p cmp_func.
+ * When no such element exists, @p size is returned.
  *
- * If \p elem is contained in the array, this is identical to
+ * If @p elem is contained in the array, this is identical to
  * #cx_array_binary_search().
  *
- * If the array is not sorted with respect to the \p cmp_func, the behavior
+ * If the array is not sorted with respect to the @p cmp_func, the behavior
  * is undefined.
  *
  * @param arr the array to search
@@ -376,26 +652,18 @@
  * @param elem_size the size of one element
  * @param elem the element to find
  * @param cmp_func the compare function
- * @return the index of the smallest upper bound, or \p size
+ * @return the index of the smallest upper bound, or @p size
+ * @see cx_array_binary_search_inf()
+ * @see cx_array_binary_search()
  */
-__attribute__((__nonnull__))
-static inline size_t cx_array_binary_search_sup(
+cx_attr_nonnull
+size_t cx_array_binary_search_sup(
         const void *arr,
         size_t size,
         size_t elem_size,
         const void *elem,
         cx_compare_func cmp_func
-) {
-    size_t inf = cx_array_binary_search_inf(arr, size, elem_size, elem, cmp_func);
-    if (inf == size) {
-        // no infimum means, first element is supremum
-        return 0;
-    } else if (cmp_func(((const char *) arr) + inf * elem_size, elem) == 0) {
-        return inf;
-    } else {
-        return inf + 1;
-    }
-}
+);
 
 /**
  * Swaps two array elements.
@@ -405,7 +673,7 @@
  * @param idx1 index of first element
  * @param idx2 index of second element
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_array_swap(
         void *arr,
         size_t elem_size,
@@ -414,21 +682,24 @@
 );
 
 /**
- * Allocates an array list for storing elements with \p elem_size bytes each.
+ * Allocates an array list for storing elements with @p elem_size bytes each.
  *
- * If \p elem_size is CX_STORE_POINTERS, the created list will be created as if
+ * If @p elem_size is CX_STORE_POINTERS, the created list will be created as if
  * cxListStorePointers() was called immediately after creation and the compare
  * function will be automatically set to cx_cmp_ptr(), if none is given.
  *
  * @param allocator the allocator for allocating the list memory
- * (if \c NULL the cxDefaultAllocator will be used)
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param comparator the comparator for the elements
- * (if \c NULL, and the list is not storing pointers, sort and find
+ * (if @c NULL, and the list is not storing pointers, sort and find
  * functions will not work)
  * @param elem_size the size of each element in bytes
  * @param initial_capacity the initial number of elements the array can store
  * @return the created list
  */
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxListFree, 1)
 CxList *cxArrayListCreate(
         const CxAllocator *allocator,
         cx_compare_func comparator,
@@ -437,18 +708,18 @@
 );
 
 /**
- * Allocates an array list for storing elements with \p elem_size bytes each.
+ * Allocates an array list for storing elements with @p elem_size bytes each.
  *
- * The list will use the cxDefaultAllocator and \em NO compare function.
+ * The list will use the cxDefaultAllocator and @em NO compare function.
  * If you want to call functions that need a compare function, you have to
  * set it immediately after creation or use cxArrayListCreate().
  *
- * If \p elem_size is CX_STORE_POINTERS, the created list will be created as if
+ * If @p elem_size is CX_STORE_POINTERS, the created list will be created as if
  * cxListStorePointers() was called immediately after creation and the compare
  * function will be automatically set to cx_cmp_ptr().
  *
- * @param elem_size the size of each element in bytes
- * @param initial_capacity the initial number of elements the array can store
+ * @param elem_size (@c size_t) the size of each element in bytes
+ * @param initial_capacity (@c size_t) the initial number of elements the array can store
  * @return the created list
  */
 #define cxArrayListCreateSimple(elem_size, initial_capacity) \
--- a/ucx/cx/buffer.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/buffer.h	Sun Jan 05 22:00:39 2025 +0100
@@ -27,9 +27,9 @@
  */
 
 /**
- * \file buffer.h
+ * @file buffer.h
  *
- * \brief Advanced buffer implementation.
+ * @brief Advanced buffer implementation.
  *
  * Instances of CxBuffer can be used to read from or to write to like one
  * would do with a stream.
@@ -38,9 +38,9 @@
  * can be enabled. See the documentation of the macro constants for more
  * information.
  *
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_BUFFER_H
@@ -49,7 +49,7 @@
 #include "common.h"
 #include "allocator.h"
 
-#ifdef    __cplusplus
+#ifdef __cplusplus
 extern "C" {
 #endif
 
@@ -60,16 +60,90 @@
 
 /**
  * If this flag is enabled, the buffer will automatically free its contents when destroyed.
+ *
+ * Do NOT set this flag together with #CX_BUFFER_COPY_ON_WRITE. It will be automatically
+ * set when the copy-on-write operations is performed.
  */
 #define CX_BUFFER_FREE_CONTENTS 0x01
 
 /**
- * If this flag is enabled, the buffer will automatically extends its capacity.
+ * If this flag is enabled, the buffer will automatically extend its capacity.
  */
 #define CX_BUFFER_AUTO_EXTEND 0x02
 
+/**
+ * If this flag is enabled, the buffer will allocate new memory when written to.
+ *
+ * The current contents of the buffer will be copied to the new memory and the flag
+ * will be cleared while the #CX_BUFFER_FREE_CONTENTS flag will be set automatically.
+ */
+#define CX_BUFFER_COPY_ON_WRITE 0x04
+
+/**
+ * If this flag is enabled, the buffer will copy its contents to a new memory area on reallocation.
+ *
+ * After performing the copy, the flag is automatically cleared.
+ * This flag has no effect on buffers which do not have #CX_BUFFER_AUTO_EXTEND set, which is why
+ * buffers automatically admit the auto-extend flag when initialized with copy-on-extend enabled.
+ */
+#define CX_BUFFER_COPY_ON_EXTEND 0x08
+
+/**
+ * Configuration for automatic flushing.
+ */
+struct cx_buffer_flush_config_s {
+    /**
+     * The buffer may not extend beyond this threshold before starting to flush.
+     *
+     * Only used when the buffer uses #CX_BUFFER_AUTO_EXTEND.
+     * The threshold will be the maximum capacity the buffer is extended to
+     * before flushing.
+     */
+    size_t threshold;
+    /**
+     * The block size for the elements to flush.
+     */
+    size_t blksize;
+    /**
+     * The maximum number of blocks to flush in one cycle.
+     *
+     * @attention while it is guaranteed that cxBufferFlush() will not flush
+     * more blocks, this is not necessarily the case for cxBufferWrite().
+     * After performing a flush cycle, cxBufferWrite() will retry the write
+     * operation and potentially trigger another flush cycle, until the
+     * flush target accepts no more data.
+     */
+    size_t blkmax;
+
+    /**
+     * The target for write function.
+     */
+    void *target;
+
+    /**
+     * The write-function used for flushing.
+     * If NULL, the flushed content gets discarded.
+     */
+    cx_write_func wfunc;
+};
+
+/**
+ * Type alais for the flush configuration struct.
+ *
+ * @code
+ * struct cx_buffer_flush_config_s {
+ *     size_t threshold;
+ *     size_t blksize;
+ *     size_t blkmax;
+ *     void *target;
+ *     cx_write_func wfunc;
+ * };
+ * @endcode
+ */
+typedef struct cx_buffer_flush_config_s CxBufferFlushConfig;
+
 /** Structure for the UCX buffer data. */
-typedef struct {
+struct cx_buffer_s {
     /** A pointer to the buffer contents. */
     union {
         /**
@@ -83,6 +157,12 @@
     };
     /** The allocator to use for automatic memory management. */
     const CxAllocator *allocator;
+    /**
+     * Optional flush configuration
+     *
+     * @see cxBufferEnableFlushing()
+     */
+    CxBufferFlushConfig* flush;
     /** Current position of the buffer. */
     size_t pos;
     /** Current capacity (i.e. maximum size) of the buffer. */
@@ -90,70 +170,52 @@
     /** Current size of the buffer content. */
     size_t size;
     /**
-     * The buffer may not extend beyond this threshold before starting to flush.
-     * Default is \c SIZE_MAX (flushing disabled when auto extension is enabled).
-     */
-    size_t flush_threshold;
-    /**
-     * The block size for the elements to flush.
-     * Default is 4096 bytes.
-     */
-    size_t flush_blksize;
-    /**
-     * The maximum number of blocks to flush in one cycle.
-     * Zero disables flushing entirely (this is the default).
-     * Set this to \c SIZE_MAX to flush the entire buffer.
-     *
-     * @attention if the maximum number of blocks multiplied with the block size
-     * is smaller than the expected contents written to this buffer within one write
-     * operation, multiple flush cycles are performed after that write.
-     * That means the total number of blocks flushed after one write to this buffer may
-     * be larger than \c flush_blkmax.
-     */
-    size_t flush_blkmax;
-
-    /**
-     * The write function used for flushing.
-     * If NULL, the flushed content gets discarded.
-     */
-    cx_write_func flush_func;
-
-    /**
-     * The target for \c flush_func.
-     */
-    void *flush_target;
-
-    /**
      * Flag register for buffer features.
      * @see #CX_BUFFER_DEFAULT
      * @see #CX_BUFFER_FREE_CONTENTS
      * @see #CX_BUFFER_AUTO_EXTEND
+     * @see #CX_BUFFER_COPY_ON_WRITE
      */
     int flags;
-} cx_buffer_s;
+};
 
 /**
  * UCX buffer.
  */
-typedef cx_buffer_s CxBuffer;
+typedef struct cx_buffer_s CxBuffer;
 
 /**
  * Initializes a fresh buffer.
  *
- * \note You may provide \c NULL as argument for \p space.
+ * You may also provide a read-only @p space, in which case
+ * you will need to cast the pointer, and you should set the
+ * #CX_BUFFER_COPY_ON_WRITE flag.
+ *
+ * You need to set the size manually after initialization, if
+ * you provide @p space which already contains data.
+ *
+ * When you specify stack memory as @p space and decide to use
+ * the auto-extension feature, you @em must use the
+ * #CX_BUFFER_COPY_ON_EXTEND flag, instead of the
+ * #CX_BUFFER_AUTO_EXTEND flag.
+ *
+ * @note You may provide @c NULL as argument for @p space.
  * Then this function will allocate the space and enforce
- * the #CX_BUFFER_FREE_CONTENTS flag.
+ * the #CX_BUFFER_FREE_CONTENTS flag. In that case, specifying
+ * copy-on-write should be avoided, because the allocated
+ * space will be leaking after the copy-on-write operation.
  *
  * @param buffer the buffer to initialize
- * @param space pointer to the memory area, or \c NULL to allocate
+ * @param space pointer to the memory area, or @c NULL to allocate
  * new memory
  * @param capacity the capacity of the buffer
  * @param allocator the allocator this buffer shall use for automatic
- * memory management. If \c NULL, the default heap allocator will be used.
+ * memory management
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param flags buffer features (see cx_buffer_s.flags)
  * @return zero on success, non-zero if a required allocation failed
  */
-__attribute__((__nonnull__(1)))
+cx_attr_nonnull_arg(1)
 int cxBufferInit(
         CxBuffer *buffer,
         void *space,
@@ -163,25 +225,23 @@
 );
 
 /**
- * Allocates and initializes a fresh buffer.
+ * Configures the buffer for flushing.
  *
- * \note You may provide \c NULL as argument for \p space.
- * Then this function will allocate the space and enforce
- * the #CX_BUFFER_FREE_CONTENTS flag.
+ * Flushing can happen automatically when data is written
+ * to the buffer (see cxBufferWrite()) or manually when
+ * cxBufferFlush() is called.
  *
- * @param space pointer to the memory area, or \c NULL to allocate
- * new memory
- * @param capacity the capacity of the buffer
- * @param allocator the allocator to use for allocating the structure and the automatic
- * memory management within the buffer. If \c NULL, the default heap allocator will be used.
- * @param flags buffer features (see cx_buffer_s.flags)
- * @return a pointer to the buffer on success, \c NULL if a required allocation failed
+ * @param buffer the buffer
+ * @param config the flush configuration
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cxBufferFlush()
+ * @see cxBufferWrite()
  */
-CxBuffer *cxBufferCreate(
-        void *space,
-        size_t capacity,
-        const CxAllocator *allocator,
-        int flags
+cx_attr_nonnull
+int cxBufferEnableFlushing(
+    CxBuffer *buffer,
+    CxBufferFlushConfig config
 );
 
 /**
@@ -193,22 +253,58 @@
  * @param buffer the buffer which contents shall be destroyed
  * @see cxBufferInit()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxBufferDestroy(CxBuffer *buffer);
 
 /**
  * Deallocates the buffer.
  *
  * If the #CX_BUFFER_FREE_CONTENTS feature is enabled, this function also destroys
- * the contents. If you \em only want to destroy the contents, use cxBufferDestroy().
+ * the contents. If you @em only want to destroy the contents, use cxBufferDestroy().
+ *
+ * @remark As with all free() functions, this accepts @c NULL arguments in which
+ * case it does nothing.
  *
  * @param buffer the buffer to deallocate
  * @see cxBufferCreate()
  */
-__attribute__((__nonnull__))
 void cxBufferFree(CxBuffer *buffer);
 
 /**
+ * Allocates and initializes a fresh buffer.
+ *
+ * You may also provide a read-only @p space, in which case
+ * you will need to cast the pointer, and you should set the
+ * #CX_BUFFER_COPY_ON_WRITE flag.
+ * When you specify stack memory as @p space and decide to use
+ * the auto-extension feature, you @em must use the
+ * #CX_BUFFER_COPY_ON_EXTEND flag, instead of the
+ * #CX_BUFFER_AUTO_EXTEND flag.
+ *
+ * @note You may provide @c NULL as argument for @p space.
+ * Then this function will allocate the space and enforce
+ * the #CX_BUFFER_FREE_CONTENTS flag.
+ *
+ * @param space pointer to the memory area, or @c NULL to allocate
+ * new memory
+ * @param capacity the capacity of the buffer
+ * @param allocator the allocator to use for allocating the structure and the automatic
+ * memory management within the buffer
+ * (if @c NULL, a default stdlib allocator will be used)
+ * @param flags buffer features (see cx_buffer_s.flags)
+ * @return a pointer to the buffer on success, @c NULL if a required allocation failed
+ */
+cx_attr_malloc
+cx_attr_dealloc(cxBufferFree, 1)
+cx_attr_nodiscard
+CxBuffer *cxBufferCreate(
+        void *space,
+        size_t capacity,
+        const CxAllocator *allocator,
+        int flags
+);
+
+/**
  * Shifts the contents of the buffer by the given offset.
  *
  * If the offset is positive, the contents are shifted to the right.
@@ -219,7 +315,7 @@
  * are discarded.
  *
  * If the offset is negative, the contents are shifted to the left where the
- * first \p shift bytes are discarded.
+ * first @p shift bytes are discarded.
  * The new size of the buffer is the old size minus the absolute shift value.
  * If this value is larger than the buffer size, the buffer is emptied (but
  * not cleared, see the security note below).
@@ -227,11 +323,11 @@
  * The buffer position gets shifted alongside with the content but is kept
  * within the boundaries of the buffer.
  *
- * \note For situations where \c off_t is not large enough, there are specialized cxBufferShiftLeft() and
- * cxBufferShiftRight() functions using a \c size_t as parameter type.
+ * @note For situations where @c off_t is not large enough, there are specialized cxBufferShiftLeft() and
+ * cxBufferShiftRight() functions using a @c size_t as parameter type.
  *
- * \attention
- * Security Note: The shifting operation does \em not erase the previously occupied memory cells.
+ * @attention
+ * Security Note: The shifting operation does @em not erase the previously occupied memory cells.
  * But you can easily do that manually, e.g. by calling
  * <code>memset(buffer->bytes, 0, shift)</code> for a right shift or
  * <code>memset(buffer->bytes + buffer->size, 0, buffer->capacity - buffer->size)</code>
@@ -239,9 +335,12 @@
  *
  * @param buffer the buffer
  * @param shift the shift offset (negative means left shift)
- * @return 0 on success, non-zero if a required auto-extension fails
+ * @retval zero success
+ * @retval non-zero if a required auto-extension or copy-on-write fails
+ * @see cxBufferShiftLeft()
+ * @see cxBufferShiftRight()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferShift(
         CxBuffer *buffer,
         off_t shift
@@ -253,10 +352,11 @@
  *
  * @param buffer the buffer
  * @param shift the shift offset
- * @return 0 on success, non-zero if a required auto-extension fails
+ * @retval zero success
+ * @retval non-zero if a required auto-extension or copy-on-write fails
  * @see cxBufferShift()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferShiftRight(
         CxBuffer *buffer,
         size_t shift
@@ -266,15 +366,13 @@
  * Shifts the buffer to the left.
  * See cxBufferShift() for details.
  *
- * \note Since a left shift cannot fail due to memory allocation problems, this
- * function always returns zero.
- *
  * @param buffer the buffer
  * @param shift the positive shift offset
- * @return always zero
+ * @retval zero success
+ * @retval non-zero if the buffer uses copy-on-write and the allocation fails
  * @see cxBufferShift()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferShiftLeft(
         CxBuffer *buffer,
         size_t shift
@@ -284,23 +382,24 @@
 /**
  * Moves the position of the buffer.
  *
- * The new position is relative to the \p whence argument.
+ * The new position is relative to the @p whence argument.
  *
- * \li \c SEEK_SET marks the start of the buffer.
- * \li \c SEEK_CUR marks the current position.
- * \li \c SEEK_END marks the end of the buffer.
+ * @li @c SEEK_SET marks the start of the buffer.
+ * @li @c SEEK_CUR marks the current position.
+ * @li @c SEEK_END marks the end of the buffer.
  *
  * With an offset of zero, this function sets the buffer position to zero
- * (\c SEEK_SET), the buffer size (\c SEEK_END) or leaves the buffer position
- * unchanged (\c SEEK_CUR).
+ * (@c SEEK_SET), the buffer size (@c SEEK_END) or leaves the buffer position
+ * unchanged (@c SEEK_CUR).
  *
  * @param buffer the buffer
- * @param offset position offset relative to \p whence
- * @param whence one of \c SEEK_SET, \c SEEK_CUR or \c SEEK_END
- * @return 0 on success, non-zero if the position is invalid
+ * @param offset position offset relative to @p whence
+ * @param whence one of @c SEEK_SET, @c SEEK_CUR or @c SEEK_END
+ * @retval zero success
+ * @retval non-zero if the position is invalid
  *
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferSeek(
         CxBuffer *buffer,
         off_t offset,
@@ -313,10 +412,13 @@
  * The data is deleted by zeroing it with a call to memset().
  * If you do not need that, you can use the faster cxBufferReset().
  *
+ * @note If the #CX_BUFFER_COPY_ON_WRITE flag is set, this function
+ * will not erase the data and behave exactly as cxBufferReset().
+ *
  * @param buffer the buffer to be cleared
  * @see cxBufferReset()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxBufferClear(CxBuffer *buffer);
 
 /**
@@ -328,18 +430,20 @@
  * @param buffer the buffer to be cleared
  * @see cxBufferClear()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxBufferReset(CxBuffer *buffer);
 
 /**
  * Tests, if the buffer position has exceeded the buffer size.
  *
  * @param buffer the buffer to test
- * @return non-zero, if the current buffer position has exceeded the last
- * byte of the buffer's contents.
+ * @retval true if the current buffer position has exceeded the last
+ * byte of the buffer's contents
+ * @retval false otherwise
  */
-__attribute__((__nonnull__))
-int cxBufferEof(const CxBuffer *buffer);
+cx_attr_nonnull
+cx_attr_nodiscard
+bool cxBufferEof(const CxBuffer *buffer);
 
 
 /**
@@ -349,9 +453,10 @@
  *
  * @param buffer the buffer
  * @param capacity the minimum required capacity for this buffer
- * @return 0 on success or a non-zero value on failure
+ * @retval zero the capacity was already sufficient or successfully increased
+ * @retval non-zero on allocation failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferMinimumCapacity(
         CxBuffer *buffer,
         size_t capacity
@@ -360,6 +465,10 @@
 /**
  * Writes data to a CxBuffer.
  *
+ * If automatic flushing is not enabled, the data is simply written into the
+ * buffer at the current position and the position of the buffer is increased
+ * by the number of bytes written.
+ *
  * If flushing is enabled and the buffer needs to flush, the data is flushed to
  * the target until the target signals that it cannot take more data by
  * returning zero via the respective write function. In that case, the remaining
@@ -367,23 +476,23 @@
  * newly available space can be used to append as much data as possible. This
  * function only stops writing more elements, when the flush target and this
  * buffer are both incapable of taking more data or all data has been written.
- * The number returned by this function is the total number of elements that
- * could be written during the process. It does not necessarily mean that those
- * elements are still in this buffer, because some of them could have also be
- * flushed already.
+ * If number of items that shall be written is larger than the buffer can hold,
+ * the first items from @c ptr are directly relayed to the flush target, if
+ * possible.
+ * The number returned by this function is only the number of elements from
+ * @c ptr that could be written to either the flush target or the buffer.
  *
- * If automatic flushing is not enabled, the position of the buffer is increased
- * by the number of bytes written.
- *
- * \note The signature is compatible with the fwrite() family of functions.
+ * @note The signature is compatible with the fwrite() family of functions.
  *
  * @param ptr a pointer to the memory area containing the bytes to be written
  * @param size the length of one element
  * @param nitems the element count
  * @param buffer the CxBuffer to write to
  * @return the total count of elements written
+ * @see cxBufferAppend()
+ * @see cxBufferRead()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cxBufferWrite(
         const void *ptr,
         size_t size,
@@ -392,19 +501,104 @@
 );
 
 /**
+ * Appends data to a CxBuffer.
+ *
+ * The data is always appended to current data within the buffer,
+ * regardless of the current position.
+ * This is especially useful when the buffer is primarily meant for reading
+ * while additional data is added to the buffer occasionally.
+ * Consequently, the position of the buffer is unchanged after this operation.
+ *
+ * @note The signature is compatible with the fwrite() family of functions.
+ *
+ * @param ptr a pointer to the memory area containing the bytes to be written
+ * @param size the length of one element
+ * @param nitems the element count
+ * @param buffer the CxBuffer to write to
+ * @return the total count of elements written
+ * @see cxBufferWrite()
+ * @see cxBufferRead()
+ */
+cx_attr_nonnull
+size_t cxBufferAppend(
+        const void *ptr,
+        size_t size,
+        size_t nitems,
+        CxBuffer *buffer
+);
+
+/**
+ * Performs a single flush-run on the specified buffer.
+ *
+ * Does nothing when the position in the buffer is zero.
+ * Otherwise, the data until the current position minus
+ * one is considered for flushing.
+ * Note carefully that flushing will never exceed the
+ * current @em position, even when the size of the
+ * buffer is larger than the current position.
+ *
+ * One flush run will try to flush @c blkmax many
+ * blocks of size @c blksize until either the @p buffer
+ * has no more data to flush or the write function
+ * used for flushing returns zero.
+ *
+ * The buffer is shifted left for that many bytes
+ * the flush operation has successfully flushed.
+ *
+ * @par Example 1
+ * Assume you have a buffer with size 340 and you are
+ * at position 200. The flush configuration is
+ * @c blkmax=4 and @c blksize=64 .
+ * Assume that the entire flush operation is successful.
+ * All 200 bytes on the left hand-side from the current
+ * position are written.
+ * That means, the size of the buffer is now 140 and the
+ * position is zero.
+ *
+ * @par Example 2
+ * Same as Example 1, but now the @c blkmax is 1.
+ * The size of the buffer is now 276 and the position is 136.
+ *
+ * @par Example 3
+ * Same as Example 1, but now assume the flush target
+ * only accepts 100 bytes before returning zero.
+ * That means, the flush operations manages to flush
+ * one complete block and one partial block, ending
+ * up with a buffer with size 240 and position 100.
+ *
+ * @remark Just returns zero when flushing was not enabled with
+ * cxBufferEnableFlushing().
+ *
+ * @remark When the buffer uses copy-on-write, the memory
+ * is copied first, before attempting any flush.
+ * This is, however, considered an erroneous use of the
+ * buffer, because it does not make much sense to put
+ * readonly data into an UCX buffer for flushing, instead
+ * of writing it directly to the target.
+ *
+ * @param buffer the buffer
+ * @return the number of successfully flushed bytes
+ * @see cxBufferEnableFlushing()
+ */
+cx_attr_nonnull
+size_t cxBufferFlush(CxBuffer *buffer);
+
+/**
  * Reads data from a CxBuffer.
  *
  * The position of the buffer is increased by the number of bytes read.
  *
- * \note The signature is compatible with the fread() family of functions.
+ * @note The signature is compatible with the fread() family of functions.
  *
  * @param ptr a pointer to the memory area where to store the read data
  * @param size the length of one element
  * @param nitems the element count
  * @param buffer the CxBuffer to read from
  * @return the total number of elements read
+ * @see cxBufferWrite()
+ * @see cxBufferAppend()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cxBufferRead(
         void *ptr,
         size_t size,
@@ -417,30 +611,52 @@
  *
  * The least significant byte of the argument is written to the buffer. If the
  * end of the buffer is reached and #CX_BUFFER_AUTO_EXTEND feature is enabled,
- * the buffer capacity is extended by cxBufferMinimumCapacity(). If the feature is
- * disabled or buffer extension fails, \c EOF is returned.
+ * the buffer capacity is extended by cxBufferMinimumCapacity(). If the feature
+ * is disabled or buffer extension fails, @c EOF is returned.
  *
  * On successful write, the position of the buffer is increased.
  *
+ * If you just want to write a null-terminator at the current position, you
+ * should use cxBufferTerminate() instead.
+ *
  * @param buffer the buffer to write to
  * @param c the character to write
- * @return the byte that has bean written or \c EOF when the end of the stream is
+ * @return the byte that has been written or @c EOF when the end of the stream is
  * reached and automatic extension is not enabled or not possible
+ * @see cxBufferTerminate()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferPut(
         CxBuffer *buffer,
         int c
 );
 
 /**
+ * Writes a terminating zero to a buffer at the current position.
+ *
+ * On successful write, @em neither the position @em nor the size of the buffer is
+ * increased.
+ *
+ * The purpose of this function is to have the written data ready to be used as
+ * a C string.
+ *
+ * @param buffer the buffer to write to
+ * @return zero, if the terminator could be written, non-zero otherwise
+ */
+cx_attr_nonnull
+int cxBufferTerminate(CxBuffer *buffer);
+
+/**
  * Writes a string to a buffer.
  *
+ * This is a convenience function for <code>cxBufferWrite(str, 1, strlen(str), buffer)</code>.
+ *
  * @param buffer the buffer
  * @param str the zero-terminated string
  * @return the number of bytes written
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 size_t cxBufferPutString(
         CxBuffer *buffer,
         const char *str
@@ -452,9 +668,9 @@
  * The current position of the buffer is increased after a successful read.
  *
  * @param buffer the buffer to read from
- * @return the character or \c EOF, if the end of the buffer is reached
+ * @return the character or @c EOF, if the end of the buffer is reached
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferGet(CxBuffer *buffer);
 
 #ifdef __cplusplus
--- a/ucx/cx/collection.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/collection.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file collection.h
- * \brief Common definitions for various collection implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file collection.h
+ * @brief Common definitions for various collection implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_COLLECTION_H
@@ -96,13 +96,21 @@
 
 /**
  * Use this macro to declare common members for a collection structure.
+ *
+ * @par Example Use
+ * @code
+ * struct MyCustomSet {
+ *     CX_COLLECTION_BASE;
+ *     MySetElements *data;
+ * }
+ * @endcode
  */
 #define CX_COLLECTION_BASE struct cx_collection_s collection
 
 /**
  * Sets a simple destructor function for this collection.
  *
- * @param c the collection
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
  * @param destr the destructor function
  */
 #define cxDefineDestructor(c, destr) \
@@ -111,7 +119,7 @@
 /**
  * Sets a simple destructor function for this collection.
  *
- * @param c the collection
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
  * @param destr the destructor function
  */
 #define cxDefineAdvancedDestructor(c, destr, data) \
@@ -124,8 +132,11 @@
  * Usually only used by collection implementations. There should be no need
  * to invoke this macro manually.
  *
- * @param c the collection
- * @param e the element
+ * When the collection stores pointers, those pointers are directly passed
+ * to the destructor. Otherwise, a pointer to the element is passed.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @param e the element (the type is @c void* or @c void** depending on context)
  */
 #define cx_invoke_simple_destructor(c, e) \
     (c)->collection.simple_destructor((c)->collection.store_pointer ? (*((void **) (e))) : (e))
@@ -136,8 +147,11 @@
  * Usually only used by collection implementations. There should be no need
  * to invoke this macro manually.
  *
- * @param c the collection
- * @param e the element
+ * When the collection stores pointers, those pointers are directly passed
+ * to the destructor. Otherwise, a pointer to the element is passed.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @param e the element (the type is @c void* or @c void** depending on context)
  */
 #define cx_invoke_advanced_destructor(c, e) \
     (c)->collection.advanced_destructor((c)->collection.destructor_data, \
@@ -150,8 +164,11 @@
  * Usually only used by collection implementations. There should be no need
  * to invoke this macro manually.
  *
- * @param c the collection
- * @param e the element
+ * When the collection stores pointers, those pointers are directly passed
+ * to the destructor. Otherwise, a pointer to the element is passed.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @param e the element (the type is @c void* or @c void** depending on context)
  */
 #define cx_invoke_destructor(c, e) \
     if ((c)->collection.simple_destructor) cx_invoke_simple_destructor(c,e); \
--- a/ucx/cx/common.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/common.h	Sun Jan 05 22:00:39 2025 +0100
@@ -27,15 +27,15 @@
  */
 
 /**
- * \file common.h
+ * @file common.h
  *
- * \brief Common definitions and feature checks.
+ * @brief Common definitions and feature checks.
  *
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  *
- * \mainpage UAP Common Extensions
+ * @mainpage UAP Common Extensions
  * Library with common and useful functions, macros and data structures.
  * <p>
  * Latest available source:<br>
@@ -88,7 +88,9 @@
 /** Version constant which ensures to increase monotonically. */
 #define UCX_VERSION (((UCX_VERSION_MAJOR)<<16)|UCX_VERSION_MINOR)
 
-// Common Includes
+// ---------------------------------------------------------------------------
+//       Common includes
+// ---------------------------------------------------------------------------
 
 #include <stdlib.h>
 #include <stddef.h>
@@ -96,7 +98,194 @@
 #include <stdint.h>
 #include <sys/types.h>
 
-#ifndef UCX_TEST_H
+// ---------------------------------------------------------------------------
+//       Architecture Detection
+// ---------------------------------------------------------------------------
+
+#ifndef INTPTR_MAX
+#error Missing INTPTR_MAX definition
+#endif
+#if INTPTR_MAX == INT64_MAX
+/**
+ * The address width in bits on this platform.
+ */
+#define CX_WORDSIZE 64
+#elif INTPTR_MAX == INT32_MAX
+/**
+ * The address width in bits on this platform.
+ */
+#define CX_WORDSIZE 32
+#else
+#error Unknown pointer size or missing size macros!
+#endif
+
+// ---------------------------------------------------------------------------
+//       Missing Defines
+// ---------------------------------------------------------------------------
+
+#ifndef SSIZE_MAX // not defined in glibc since C23 and MSVC
+#if CX_WORDSIZE == 64
+/**
+ * The maximum representable value in ssize_t.
+ */
+#define SSIZE_MAX 0x7fffffffffffffffll
+#else
+#define SSIZE_MAX 0x7fffffffl
+#endif
+#endif
+
+
+// ---------------------------------------------------------------------------
+//       Attribute definitions
+// ---------------------------------------------------------------------------
+
+#ifndef __GNUC__
+/**
+ * Removes GNU C attributes where they are not supported.
+ */
+#define __attribute__(x)
+#endif
+
+/**
+ * All pointer arguments must be non-NULL.
+ */
+#define cx_attr_nonnull __attribute__((__nonnull__))
+
+/**
+ * The specified pointer arguments must be non-NULL.
+ */
+#define cx_attr_nonnull_arg(...) __attribute__((__nonnull__(__VA_ARGS__)))
+
+/**
+ * The returned value is guaranteed to be non-NULL.
+ */
+#define cx_attr_returns_nonnull __attribute__((__returns_nonnull__))
+
+/**
+ * The attributed function always returns freshly allocated memory.
+ */
+#define cx_attr_malloc __attribute__((__malloc__))
+
+#ifndef __clang__
+/**
+ * The pointer returned by the attributed function is supposed to be freed
+ * by @p freefunc.
+ *
+ * @param freefunc the function that shall be used to free the memory
+ * @param freefunc_arg the index of the pointer argument in @p freefunc
+ */
+#define cx_attr_dealloc(freefunc, freefunc_arg) \
+    __attribute__((__malloc__(freefunc, freefunc_arg)))
+#else
+/**
+ * Not supported in clang.
+ */
+#define cx_attr_dealloc(...)
+#endif // __clang__
+
+/**
+ * Shortcut to specify #cxFree() as deallocator.
+ */
+#define cx_attr_dealloc_ucx cx_attr_dealloc(cxFree, 2)
+
+/**
+ * Specifies the parameters from which the allocation size is calculated.
+ */
+#define cx_attr_allocsize(...) __attribute__((__alloc_size__(__VA_ARGS__)))
+
+
+#ifdef __clang__
+/**
+ * No support for @c null_terminated_string_arg in clang or GCC below 14.
+ */
+#define cx_attr_cstr_arg(idx)
+/**
+ * No support for access attribute in clang.
+ */
+#define cx_attr_access(mode, ...)
+#else
+#if __GNUC__ < 10
+/**
+ * No support for access attribute in GCC < 10.
+ */
+#define cx_attr_access(mode, ...)
+#else
+/**
+ * Helper macro to define access macros.
+ */
+#define cx_attr_access(mode, ...) __attribute__((__access__(mode, __VA_ARGS__)))
+#endif // __GNUC__ < 10
+#if __GNUC__ < 14
+/**
+ * No support for @c null_terminated_string_arg in clang or GCC below 14.
+ */
+#define cx_attr_cstr_arg(idx)
+#else
+/**
+ * The specified argument is expected to be a zero-terminated string.
+ *
+ * @param idx the index of the argument
+ */
+#define cx_attr_cstr_arg(idx) \
+    __attribute__((__null_terminated_string_arg__(idx)))
+#endif // __GNUC__ < 14
+#endif // __clang__
+
+
+/**
+ * Specifies that the function will only read through the given pointer.
+ *
+ * Takes one or two arguments: the index of the pointer and (optionally) the
+ * index of another argument specifying the maximum number of accessed bytes.
+ */
+#define cx_attr_access_r(...) cx_attr_access(__read_only__, __VA_ARGS__)
+
+/**
+ * Specifies that the function will read and write through the given pointer.
+ *
+ * Takes one or two arguments: the index of the pointer and (optionally) the
+ * index of another argument specifying the maximum number of accessed bytes.
+ */
+#define cx_attr_access_rw(...) cx_attr_access(__read_write__, __VA_ARGS__)
+
+/**
+ * Specifies that the function will only write through the given pointer.
+ *
+ * Takes one or two arguments: the index of the pointer and (optionally) the
+ * index of another argument specifying the maximum number of accessed bytes.
+ */
+#define cx_attr_access_w(...) cx_attr_access(__write_only__, __VA_ARGS__)
+
+#if __STDC_VERSION__ >= 202300L
+
+/**
+ * Do not warn about unused variable.
+ */
+#define cx_attr_unused [[maybe_unused]]
+
+/**
+ * Warn about discarded return value.
+ */
+#define cx_attr_nodiscard [[nodiscard]]
+
+#else // no C23
+
+/**
+ * Do not warn about unused variable.
+ */
+#define cx_attr_unused __attribute__((__unused__))
+
+/**
+ * Warn about discarded return value.
+ */
+#define cx_attr_nodiscard __attribute__((__warn_unused_result__))
+
+#endif // __STDC_VERSION__
+
+// ---------------------------------------------------------------------------
+//       Useful function pointers
+// ---------------------------------------------------------------------------
+
 /**
  * Function pointer compatible with fwrite-like functions.
  */
@@ -106,7 +295,6 @@
         size_t,
         void *
 );
-#endif // UCX_TEST_H
 
 /**
  * Function pointer compatible with fread-like functions.
@@ -118,25 +306,71 @@
         void *
 );
 
+// ---------------------------------------------------------------------------
+//       Utility macros
+// ---------------------------------------------------------------------------
 
-// Compiler specific stuff
+/**
+ * Determines the number of members in a static C array.
+ *
+ * @attention never use this to determine the size of a dynamically allocated
+ * array.
+ *
+ * @param arr the array identifier
+ * @return the number of elements
+ */
+#define cx_nmemb(arr) (sizeof(arr)/sizeof((arr)[0]))
 
-#ifndef __GNUC__
+// ---------------------------------------------------------------------------
+//       szmul implementation
+// ---------------------------------------------------------------------------
+
+#if (__GNUC__ >= 5 || defined(__clang__)) && !defined(CX_NO_SZMUL_BUILTIN)
+#define CX_SZMUL_BUILTIN
+#define cx_szmul(a, b, result) __builtin_mul_overflow(a, b, result)
+#else // no GNUC or clang bultin
 /**
- * Removes GNU C attributes where they are not supported.
+ * Performs a multiplication of size_t values and checks for overflow.
+  *
+ * @param a (@c size_t) first operand
+ * @param b (@c size_t) second operand
+ * @param result (@c size_t*) a pointer to a variable, where the result should
+ * be stored
+ * @retval zero success
+ * @retval non-zero the multiplication would overflow
  */
-#define __attribute__(x)
+#define cx_szmul(a, b, result) cx_szmul_impl(a, b, result)
+
+/**
+ * Implementation of cx_szmul() when no compiler builtin is available.
+ *
+ * Do not use in application code.
+ *
+ * @param a first operand
+ * @param b second operand
+ * @param result a pointer to a variable, where the result should
+ * be stored
+ * @retval zero success
+ * @retval non-zero the multiplication would overflow
+ */
+#if __cplusplus
+extern "C"
 #endif
+int cx_szmul_impl(size_t a, size_t b, size_t *result);
+#endif // cx_szmul
+
+
+// ---------------------------------------------------------------------------
+//       Fixes for MSVC incompatibilities
+// ---------------------------------------------------------------------------
 
 #ifdef _MSC_VER
-
 // fix missing ssize_t definition
 #include <BaseTsd.h>
 typedef SSIZE_T ssize_t;
 
 // fix missing _Thread_local support
 #define _Thread_local __declspec(thread)
-
-#endif
+#endif // _MSC_VER
 
 #endif // UCX_COMMON_H
--- a/ucx/cx/compare.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/compare.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file compare.h
- * \brief A collection of simple compare functions.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file compare.h
+ * @brief A collection of simple compare functions.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_COMPARE_H
@@ -42,199 +42,485 @@
 extern "C" {
 #endif
 
-#ifndef CX_COMPARE_FUNC_DEFINED
-#define CX_COMPARE_FUNC_DEFINED
 /**
- * A comparator function comparing two collection elements.
+ * A comparator function comparing two arbitrary values.
+ *
+ * All functions from compare.h with the cx_cmp prefix are
+ * compatible with this signature and can be used as
+ * compare function for collections, or other implementations
+ * that need to be type-agnostic.
+ *
+ * For simple comparisons the cx_vcmp family of functions
+ * can be used, but they are NOT compatible with this function
+ * pointer.
  */
-typedef int(*cx_compare_func)(
-        const void *left,
-        const void *right
+cx_attr_nonnull
+cx_attr_nodiscard
+typedef int (*cx_compare_func)(
+    const void *left,
+    const void *right
 );
-#endif // CX_COMPARE_FUNC_DEFINED
 
 /**
  * Compares two integers of type int.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to integer one
  * @param i2 pointer to integer two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_int(const void *i1, const void *i2);
 
 /**
+ * Compares two ints.
+ *
+ * @param i1 integer one
+ * @param i2 integer two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_int(int i1, int i2);
+
+/**
  * Compares two integers of type long int.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to long integer one
  * @param i2 pointer to long integer two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_longint(const void *i1, const void *i2);
 
 /**
+ * Compares two long ints.
+ *
+ * @param i1 long integer one
+ * @param i2 long integer two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_longint(long int i1, long int i2);
+
+/**
  * Compares two integers of type long long.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to long long one
  * @param i2 pointer to long long two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_longlong(const void *i1, const void *i2);
 
 /**
+ * Compares twolong long ints.
+ *
+ * @param i1 long long int one
+ * @param i2 long long int two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_longlong(long long int i1, long long int i2);
+
+/**
  * Compares two integers of type int16_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to int16_t one
  * @param i2 pointer to int16_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_int16(const void *i1, const void *i2);
 
 /**
+ * Compares two integers of type int16_t.
+ *
+ * @param i1 int16_t one
+ * @param i2 int16_t two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_int16(int16_t i1, int16_t i2);
+
+/**
  * Compares two integers of type int32_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to int32_t one
  * @param i2 pointer to int32_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_int32(const void *i1, const void *i2);
 
 /**
+ * Compares two integers of type int32_t.
+ *
+ * @param i1 int32_t one
+ * @param i2 int32_t two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_int32(int32_t i1, int32_t i2);
+
+/**
  * Compares two integers of type int64_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to int64_t one
  * @param i2 pointer to int64_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_int64(const void *i1, const void *i2);
 
 /**
+ * Compares two integers of type int64_t.
+ *
+ * @param i1 int64_t one
+ * @param i2 int64_t two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_int64(int64_t i1, int64_t i2);
+
+/**
  * Compares two integers of type unsigned int.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to unsigned integer one
  * @param i2 pointer to unsigned integer two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_uint(const void *i1, const void *i2);
 
 /**
+ * Compares two unsigned ints.
+ *
+ * @param i1 unsigned integer one
+ * @param i2 unsigned integer two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_uint(unsigned int i1, unsigned int i2);
+
+/**
  * Compares two integers of type unsigned long int.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to unsigned long integer one
  * @param i2 pointer to unsigned long integer two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_ulongint(const void *i1, const void *i2);
 
 /**
+ * Compares two unsigned long ints.
+ *
+ * @param i1 unsigned long integer one
+ * @param i2 unsigned long integer two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_ulongint(unsigned long int i1, unsigned long int i2);
+
+/**
  * Compares two integers of type unsigned long long.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to unsigned long long one
  * @param i2 pointer to unsigned long long two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_ulonglong(const void *i1, const void *i2);
 
 /**
+ * Compares two unsigned long long ints.
+ *
+ * @param i1 unsigned long long one
+ * @param i2 unsigned long long two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_ulonglong(unsigned long long int i1, unsigned long long int i2);
+
+/**
  * Compares two integers of type uint16_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to uint16_t one
  * @param i2 pointer to uint16_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_uint16(const void *i1, const void *i2);
 
 /**
+ * Compares two integers of type uint16_t.
+ *
+ * @param i1 uint16_t one
+ * @param i2 uint16_t two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_uint16(uint16_t i1, uint16_t i2);
+
+/**
  * Compares two integers of type uint32_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to uint32_t one
  * @param i2 pointer to uint32_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_uint32(const void *i1, const void *i2);
 
 /**
+ * Compares two integers of type uint32_t.
+ *
+ * @param i1 uint32_t one
+ * @param i2 uint32_t two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_uint32(uint32_t i1, uint32_t i2);
+
+/**
  * Compares two integers of type uint64_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to uint64_t one
  * @param i2 pointer to uint64_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_uint64(const void *i1, const void *i2);
 
 /**
+ * Compares two integers of type uint64_t.
+ *
+ * @param i1 uint64_t one
+ * @param i2 uint64_t two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_uint64(uint64_t i1, uint64_t i2);
+
+/**
  * Compares two real numbers of type float with precision 1e-6f.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param f1 pointer to float one
  * @param f2 pointer to float two
- * @return -1, if *f1 is less than *f2, 0 if both are equal,
- * 1 if *f1 is greater than *f2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_float(const void *f1, const void *f2);
 
-int cx_cmp_float(const void *f1, const void *f2);
+/**
+ * Compares two real numbers of type float with precision 1e-6f.
+ *
+ * @param f1 float one
+ * @param f2 float two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_float(float f1, float f2);
 
 /**
  * Compares two real numbers of type double with precision 1e-14.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param d1 pointer to double one
  * @param d2 pointer to double two
- * @return -1, if *d1 is less than *d2, 0 if both are equal,
- * 1 if *d1 is greater than *d2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
-int cx_cmp_double(
-        const void *d1,
-        const void *d2
-);
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_double(const void *d1, const void *d2);
+
+/**
+ * Convenience function
+ *
+ * @param d1 double one
+ * @param d2 double two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_double(double d1, double d2);
 
 /**
  * Compares the integer representation of two pointers.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param ptr1 pointer to pointer one (const intptr_t*)
  * @param ptr2 pointer to pointer two (const intptr_t*)
- * @return -1 if *ptr1 is less than *ptr2, 0 if both are equal,
- * 1 if *ptr1 is greater than *ptr2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
-int cx_cmp_intptr(
-        const void *ptr1,
-        const void *ptr2
-);
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_intptr(const void *ptr1, const void *ptr2);
+
+/**
+ * Compares the integer representation of two pointers.
+ *
+ * @param ptr1 pointer one
+ * @param ptr2 pointer two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_intptr(intptr_t ptr1, intptr_t ptr2);
 
 /**
  * Compares the unsigned integer representation of two pointers.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param ptr1 pointer to pointer one (const uintptr_t*)
  * @param ptr2 pointer to pointer two (const uintptr_t*)
- * @return -1 if *ptr1 is less than *ptr2, 0 if both are equal,
- * 1 if *ptr1 is greater than *ptr2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
-int cx_cmp_uintptr(
-        const void *ptr1,
-        const void *ptr2
-);
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_uintptr(const void *ptr1, const void *ptr2);
+
+/**
+ * Compares the unsigned integer representation of two pointers.
+ *
+ * @param ptr1 pointer one
+ * @param ptr2 pointer two
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
+ */
+cx_attr_nodiscard
+int cx_vcmp_uintptr(uintptr_t ptr1, uintptr_t ptr2);
 
 /**
  * Compares the pointers specified in the arguments without de-referencing.
  *
  * @param ptr1 pointer one
  * @param ptr2 pointer two
- * @return -1 if ptr1 is less than ptr2, 0 if both are equal,
- * 1 if ptr1 is greater than ptr2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
-int cx_cmp_ptr(
-        const void *ptr1,
-        const void *ptr2
-);
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_ptr(const void *ptr1, const void *ptr2);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/hash_key.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/hash_key.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file hash_key.h
- * \brief Interface for map implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file hash_key.h
+ * @brief Interface for map implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 
@@ -38,6 +38,7 @@
 #define UCX_HASH_KEY_H
 
 #include "common.h"
+#include "string.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -61,15 +62,20 @@
 typedef struct cx_hash_key_s CxHashKey;
 
 /**
- * Computes a murmur2 32 bit hash.
+ * Computes a murmur2 32-bit hash.
  *
- * You need to initialize \c data and \c len in the key struct.
+ * You need to initialize @c data and @c len in the key struct.
  * The hash is then directly written to that struct.
  *
- * \note If \c data is \c NULL, the hash is defined as 1574210520.
+ * Usually you should not need this function.
+ * Use cx_hash_key(), instead.
+ *
+ * @note If @c data is @c NULL, the hash is defined as 1574210520.
  *
  * @param key the key, the hash shall be computed for
+ * @see cx_hash_key()
  */
+cx_attr_nonnull
 void cx_hash_murmur(CxHashKey *key);
 
 /**
@@ -80,7 +86,8 @@
  * @param str the string
  * @return the hash key
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
 CxHashKey cx_hash_key_str(const char *str);
 
 /**
@@ -90,7 +97,8 @@
  * @param len the length
  * @return the hash key
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_access_r(1, 2)
 CxHashKey cx_hash_key_bytes(
         const unsigned char *bytes,
         size_t len
@@ -107,7 +115,8 @@
  * @param len the length of object in memory
  * @return the hash key
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_access_r(1, 2)
 CxHashKey cx_hash_key(
         const void *obj,
         size_t len
@@ -119,7 +128,18 @@
  * @param str the string
  * @return the hash key
  */
-#define cx_hash_key_cxstr(str) cx_hash_key((void*)(str).ptr, (str).length)
+cx_attr_nodiscard
+static inline CxHashKey cx_hash_key_cxstr(cxstring str) {
+    return cx_hash_key(str.ptr, str.length);
+}
+
+/**
+ * Computes a hash key from a UCX string.
+ *
+ * @param str (@c cxstring or @c cxmutstr) the string
+ * @return (@c CxHashKey) the hash key
+ */
+#define cx_hash_key_cxstr(str) cx_hash_key_cxstr(cx_strcast(str))
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/hash_map.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/hash_map.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file hash_map.h
- * \brief Hash map implementation.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file hash_map.h
+ * @brief Hash map implementation.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_HASH_MAP_H
@@ -67,21 +67,24 @@
 /**
  * Creates a new hash map with the specified number of buckets.
  *
- * If \p buckets is zero, an implementation defined default will be used.
+ * If @p buckets is zero, an implementation defined default will be used.
  *
- * If \p elem_size is CX_STORE_POINTERS, the created map will be created as if
+ * If @p elem_size is CX_STORE_POINTERS, the created map will be created as if
  * cxMapStorePointers() was called immediately after creation.
  *
  * @note Iterators provided by this hash map implementation provide the remove operation.
  * The index value of an iterator is incremented when the iterator advanced without removal.
- * In other words, when the iterator is finished, \c index==size .
+ * In other words, when the iterator is finished, @c index==size .
  *
  * @param allocator the allocator to use
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param itemsize the size of one element
  * @param buckets the initial number of buckets in this hash map
  * @return a pointer to the new hash map
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxMapFree, 1)
 CxMap *cxHashMapCreate(
         const CxAllocator *allocator,
         size_t itemsize,
@@ -91,23 +94,22 @@
 /**
  * Creates a new hash map with a default number of buckets.
  *
- * If \p elem_size is CX_STORE_POINTERS, the created map will be created as if
+ * If @p elem_size is CX_STORE_POINTERS, the created map will be created as if
  * cxMapStorePointers() was called immediately after creation.
  *
  * @note Iterators provided by this hash map implementation provide the remove operation.
  * The index value of an iterator is incremented when the iterator advanced without removal.
- * In other words, when the iterator is finished, \c index==size .
+ * In other words, when the iterator is finished, @c index==size .
  *
- * @param itemsize the size of one element
- * @return a pointer to the new hash map
+ * @param itemsize (@c size_t) the size of one element
+ * @return (@c CxMap*) a pointer to the new hash map
  */
-#define cxHashMapCreateSimple(itemsize) \
-    cxHashMapCreate(cxDefaultAllocator, itemsize, 0)
+#define cxHashMapCreateSimple(itemsize) cxHashMapCreate(NULL, itemsize, 0)
 
 /**
  * Increases the number of buckets, if necessary.
  *
- * The load threshold is \c 0.75*buckets. If the element count exceeds the load
+ * The load threshold is @c 0.75*buckets. If the element count exceeds the load
  * threshold, the map will be rehashed. Otherwise, no action is performed and
  * this function simply returns 0.
  *
@@ -120,9 +122,10 @@
  * @note If the specified map is not a hash map, the behavior is undefined.
  *
  * @param map the map to rehash
- * @return zero on success, non-zero if a memory allocation error occurred
+ * @retval zero success
+ * @retval non-zero if a memory allocation error occurred
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxMapRehash(CxMap *map);
 
 
--- a/ucx/cx/iterator.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/iterator.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file iterator.h
- * \brief Interface for iterator implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file iterator.h
+ * @brief Interface for iterator implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_ITERATOR_H
@@ -38,11 +38,18 @@
 
 #include "common.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Common data for all iterators.
+ */
 struct cx_iterator_base_s {
     /**
      * True iff the iterator points to valid data.
      */
-    __attribute__ ((__nonnull__))
+    cx_attr_nonnull
     bool (*valid)(const void *);
 
     /**
@@ -50,13 +57,15 @@
      *
      * When valid returns false, the behavior of this function is undefined.
      */
-    __attribute__ ((__nonnull__))
+    cx_attr_nonnull
+    cx_attr_nodiscard
     void *(*current)(const void *);
 
     /**
      * Original implementation in case the function needs to be wrapped.
      */
-    __attribute__ ((__nonnull__))
+    cx_attr_nonnull
+    cx_attr_nodiscard
     void *(*current_impl)(const void *);
 
     /**
@@ -64,7 +73,7 @@
      *
      * When valid returns false, the behavior of this function is undefined.
      */
-    __attribute__ ((__nonnull__))
+    cx_attr_nonnull
     void (*next)(void *);
     /**
      * Indicates whether this iterator may remove elements.
@@ -86,6 +95,9 @@
  * Internal iterator struct - use CxIterator.
  */
 struct cx_iterator_s {
+    /**
+     * Inherited common data for all iterators.
+     */
     CX_ITERATOR_BASE;
 
     /**
@@ -141,7 +153,7 @@
 
     /**
      * May contain the total number of elements, if known.
-     * Shall be set to \c SIZE_MAX when the total number is unknown during iteration.
+     * Shall be set to @c SIZE_MAX when the total number is unknown during iteration.
      */
     size_t elem_count;
 };
@@ -154,7 +166,7 @@
  * to be "position-aware", which means that they keep track of the current index within the collection.
  *
  * @note Objects that are pointed to by an iterator are always mutable through that iterator. However,
- * any concurrent mutation of the collection other than by this iterator makes this iterator invalid
+ * any concurrent mutation of the collection other than by this iterator makes this iterator invalid,
  * and it must not be used anymore.
  */
 typedef struct cx_iterator_s CxIterator;
@@ -165,7 +177,8 @@
  * This is especially false for past-the-end iterators.
  *
  * @param iter the iterator
- * @return true iff the iterator points to valid data
+ * @retval true if the iterator points to valid data
+ * @retval false if the iterator already moved past the end
  */
 #define cxIteratorValid(iter) (iter).base.valid(&(iter))
 
@@ -176,6 +189,7 @@
  *
  * @param iter the iterator
  * @return a pointer to the current element
+ * @see cxIteratorValid()
  */
 #define cxIteratorCurrent(iter) (iter).base.current(&iter)
 
@@ -189,6 +203,8 @@
 /**
  * Flags the current element for removal, if this iterator is mutating.
  *
+ * Does nothing for non-mutating iterators.
+ *
  * @param iter the iterator
  */
 #define cxIteratorFlagRemoval(iter) (iter).base.remove |= (iter).base.mutating
@@ -199,11 +215,13 @@
  * This is useful for APIs that expect some iterator as an argument.
  *
  * @param iter the iterator
+ * @return (@c CxIterator*) a pointer to the iterator
  */
 #define cxIteratorRef(iter) &((iter).base)
 
 /**
  * Loops over an iterator.
+ *
  * @param type the type of the elements
  * @param elem the name of the iteration variable
  * @param iter the iterator
@@ -215,16 +233,21 @@
 /**
  * Creates an iterator for the specified plain array.
  *
- * The \p array can be \c NULL in which case the iterator will be immediately
- * initialized such that #cxIteratorValid() returns \c false.
+ * The @p array can be @c NULL in which case the iterator will be immediately
+ * initialized such that #cxIteratorValid() returns @c false.
  *
+ * This iterator yields the addresses of the array elements.
+ * If you want to iterator over an array of pointers, you can
+ * use cxIteratorPtr() to create an iterator which directly
+ * yields the stored pointers.
  *
- * @param array a pointer to the array (can be \c NULL)
+ * @param array a pointer to the array (can be @c NULL)
  * @param elem_size the size of one array element
  * @param elem_count the number of elements in the array
  * @return an iterator for the specified array
+ * @see cxIteratorPtr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxIterator cxIterator(
         const void *array,
         size_t elem_size,
@@ -238,23 +261,23 @@
  * elements through #cxIteratorFlagRemoval(). Every other change to the array
  * will bring this iterator to an undefined state.
  *
- * When \p remove_keeps_order is set to \c false, removing an element will only
+ * When @p remove_keeps_order is set to @c false, removing an element will only
  * move the last element to the position of the removed element, instead of
  * moving all subsequent elements by one. Usually, when the order of elements is
- * not important, this parameter should be set to \c false.
+ * not important, this parameter should be set to @c false.
  *
- * The \p array can be \c NULL in which case the iterator will be immediately
- * initialized such that #cxIteratorValid() returns \c false.
+ * The @p array can be @c NULL in which case the iterator will be immediately
+ * initialized such that #cxIteratorValid() returns @c false.
  *
  *
- * @param array a pointer to the array (can be \c NULL)
+ * @param array a pointer to the array (can be @c NULL)
  * @param elem_size the size of one array element
  * @param elem_count the number of elements in the array
- * @param remove_keeps_order \c true if the order of elements must be preserved
+ * @param remove_keeps_order @c true if the order of elements must be preserved
  * when removing an element
  * @return an iterator for the specified array
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxIterator cxMutIterator(
         void *array,
         size_t elem_size,
@@ -262,4 +285,48 @@
         bool remove_keeps_order
 );
 
+/**
+ * Creates an iterator for the specified plain pointer array.
+ *
+ * This iterator assumes that every element in the array is a pointer
+ * and yields exactly those pointers during iteration (while in contrast
+ * an iterator created with cxIterator() would return the addresses
+ * of those pointers within the array).
+ *
+ * @param array a pointer to the array (can be @c NULL)
+ * @param elem_count the number of elements in the array
+ * @return an iterator for the specified array
+ * @see cxIterator()
+ */
+cx_attr_nodiscard
+CxIterator cxIteratorPtr(
+        const void *array,
+        size_t elem_count
+);
+
+/**
+ * Creates a mutating iterator for the specified plain pointer array.
+ *
+ * This is the mutating variant of cxIteratorPtr(). See also
+ * cxMutIterator().
+ *
+ * @param array a pointer to the array (can be @c NULL)
+ * @param elem_count the number of elements in the array
+ * @param remove_keeps_order @c true if the order of elements must be preserved
+ * when removing an element
+ * @return an iterator for the specified array
+ * @see cxMutIterator()
+ * @see cxIteratorPtr()
+ */
+cx_attr_nodiscard
+CxIterator cxMutIteratorPtr(
+        void *array,
+        size_t elem_count,
+        bool remove_keeps_order
+);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
 #endif // UCX_ITERATOR_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ucx/cx/json.h	Sun Jan 05 22:00:39 2025 +0100
@@ -0,0 +1,1357 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+/**
+ * @file json.h
+ * @brief Interface for parsing data from JSON files.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
+ */
+
+#ifndef UCX_JSON_H
+#define UCX_JSON_H
+
+#include "common.h"
+#include "allocator.h"
+#include "string.h"
+#include "buffer.h"
+#include "array_list.h"
+
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/**
+ * The type of the parsed token.
+ */
+enum cx_json_token_type {
+    /**
+     * No complete token parsed, yet.
+     */
+    CX_JSON_NO_TOKEN,
+    /**
+     * The presumed token contains syntactical errors.
+     */
+    CX_JSON_TOKEN_ERROR,
+    /**
+     * A "begin of array" '[' token.
+     */
+    CX_JSON_TOKEN_BEGIN_ARRAY,
+    /**
+     * A "begin of object" '{' token.
+     */
+    CX_JSON_TOKEN_BEGIN_OBJECT,
+    /**
+     * An "end of array" ']' token.
+     */
+    CX_JSON_TOKEN_END_ARRAY,
+    /**
+     * An "end of object" '}' token.
+     */
+    CX_JSON_TOKEN_END_OBJECT,
+    /**
+     * A colon ':' token separating names and values.
+     */
+    CX_JSON_TOKEN_NAME_SEPARATOR,
+    /**
+     * A comma ',' token separating object entries or array elements.
+     */
+    CX_JSON_TOKEN_VALUE_SEPARATOR,
+    /**
+     * A string token.
+     */
+    CX_JSON_TOKEN_STRING,
+    /**
+     * A number token that can be represented as integer.
+     */
+    CX_JSON_TOKEN_INTEGER,
+    /**
+     * A number token that cannot be represented as integer.
+     */
+    CX_JSON_TOKEN_NUMBER,
+    /**
+     * A literal token.
+     */
+    CX_JSON_TOKEN_LITERAL,
+    /**
+     * A white-space token.
+     */
+    CX_JSON_TOKEN_SPACE
+};
+
+/**
+ * The type of some JSON value.
+ */
+enum cx_json_value_type {
+    /**
+     * Reserved.
+     */
+    CX_JSON_NOTHING, // this allows us to always return non-NULL values
+    /**
+     * A JSON object.
+     */
+    CX_JSON_OBJECT,
+    /**
+     * A JSON array.
+     */
+    CX_JSON_ARRAY,
+    /**
+     * A string.
+     */
+    CX_JSON_STRING,
+    /**
+     * A number that contains an integer.
+     */
+    CX_JSON_INTEGER,
+    /**
+     * A number, not necessarily an integer.
+     */
+    CX_JSON_NUMBER,
+    /**
+     * A literal (true, false, null).
+     */
+    CX_JSON_LITERAL
+};
+
+/**
+ * JSON literal types.
+ */
+enum cx_json_literal {
+    /**
+     * The @c null literal.
+     */
+    CX_JSON_NULL,
+    /**
+     * The @c true literal.
+     */
+    CX_JSON_TRUE,
+    /**
+     * The @c false literal.
+     */
+    CX_JSON_FALSE
+};
+
+/**
+ * Type alias for the token type enum.
+ */
+typedef enum cx_json_token_type CxJsonTokenType;
+/**
+ * Type alias for the value type enum.
+ */
+typedef enum cx_json_value_type CxJsonValueType;
+
+/**
+ * Type alias for the JSON parser interface.
+ */
+typedef struct cx_json_s CxJson;
+
+/**
+ * Type alias for the token struct.
+ */
+typedef struct cx_json_token_s CxJsonToken;
+
+/**
+ * Type alias for the JSON value struct.
+ */
+typedef struct cx_json_value_s CxJsonValue;
+
+/**
+ * Type alias for the JSON array struct.
+ */
+typedef struct cx_json_array_s CxJsonArray;
+/**
+ * Type alias for the JSON object struct.
+ */
+typedef struct cx_json_object_s CxJsonObject;
+/**
+ * Type alias for a JSON string.
+ */
+typedef struct cx_mutstr_s CxJsonString;
+/**
+ * Type alias for a number that can be represented as 64-bit signed integer.
+ */
+typedef int64_t CxJsonInteger;
+/**
+ * Type alias for number that is not an integer.
+ */
+typedef double CxJsonNumber;
+/**
+ * Type alias for a JSON literal.
+ */
+typedef enum cx_json_literal CxJsonLiteral;
+
+/**
+ * Type alias for a key/value pair in a JSON object.
+ */
+typedef struct cx_json_obj_value_s CxJsonObjValue;
+
+/**
+ * JSON array structure.
+ */
+struct cx_json_array_s {
+    /**
+     * The array data.
+     */
+    CX_ARRAY_DECLARE(CxJsonValue*, array);
+};
+
+/**
+ * JSON object structure.
+ */
+struct cx_json_object_s {
+    /**
+     * The key/value entries.
+     */
+    CX_ARRAY_DECLARE(CxJsonObjValue, values);
+    /**
+     * The original indices to reconstruct the order in which the members were added.
+     */
+    size_t *indices;
+};
+
+/**
+ * Structure for a key/value entry in a JSON object.
+ */
+struct cx_json_obj_value_s {
+    /**
+     * The key (or name in JSON terminology) of the value.
+     */
+    cxmutstr name;
+    /**
+     * The value.
+     */
+    CxJsonValue *value;
+};
+
+/**
+ * Structure for a JSON value.
+ */
+struct cx_json_value_s {
+    /**
+     * The allocator with which the value was allocated.
+     *
+     * Required for recursively deallocating memory of objects and arrays.
+     */
+    const CxAllocator *allocator;
+    /**
+     * The type of this value.
+     *
+     * Specifies how the @c value union shall be resolved.
+     */
+    CxJsonValueType type;
+    /**
+     * The value data.
+     */
+    union {
+        /**
+         * The array data if type is #CX_JSON_ARRAY.
+         */
+        CxJsonArray array;
+        /**
+         * The object data if type is #CX_JSON_OBJECT.
+         */
+        CxJsonObject object;
+        /**
+         * The string data if type is #CX_JSON_STRING.
+         */
+        CxJsonString string;
+        /**
+         * The integer if type is #CX_JSON_INTEGER.
+         */
+        CxJsonInteger integer;
+        /**
+         * The number if type is #CX_JSON_NUMBER.
+         */
+        CxJsonNumber number;
+        /**
+         * The literal type if type is #CX_JSON_LITERAL.
+         */
+        CxJsonLiteral literal;
+    } value;
+};
+
+/**
+ * Internally used structure for a parsed token.
+ * 
+ * You should never need to use this in your code.
+ */
+struct cx_json_token_s {
+    /**
+     * The token type.
+     */
+    CxJsonTokenType tokentype;
+    /**
+     * True, iff the @c content must be passed to cx_strfree().
+     */
+    bool allocated;
+    /**
+     * The token text, if any.
+     *
+     * This is not necessarily set when the token type already
+     * uniquely identifies the content.
+     */
+    cxmutstr content;
+};
+
+/**
+ * The JSON parser interface.
+ */
+struct cx_json_s {
+    /**
+     * The allocator used for produced JSON values.
+     */
+    const CxAllocator *allocator;
+    /**
+     * The input buffer.
+     */
+    CxBuffer buffer;
+
+    /**
+     * Used internally.
+     *
+     * Remembers the prefix of the last uncompleted token.
+     */
+    CxJsonToken uncompleted;
+
+    /**
+     * A pointer to an intermediate state of the currently parsed value.
+     *
+     * Never access this value manually.
+     */
+    CxJsonValue *parsed;
+
+    /**
+     * A pointer to an intermediate state of a currently parsed object member.
+     *
+     * Never access this value manually.
+     */
+    CxJsonObjValue uncompleted_member;
+
+    /**
+     * State stack.
+     */
+    CX_ARRAY_DECLARE_SIZED(int, states, unsigned);
+
+    /**
+     * Value buffer stack.
+     */
+    CX_ARRAY_DECLARE_SIZED(CxJsonValue*, vbuf, unsigned);
+
+    /**
+     * Internally reserved memory for the state stack.
+     */
+    int states_internal[8];
+
+    /**
+     * Internally reserved memory for the value buffer stack.
+     */
+    CxJsonValue* vbuf_internal[8];
+
+    /**
+     * Used internally.
+     */
+    bool tokenizer_escape; // TODO: check if it can be replaced with look-behind
+};
+
+/**
+ * Status codes for the json interface.
+ */
+enum cx_json_status {
+    /**
+     * Everything is fine.
+     */
+    CX_JSON_NO_ERROR,
+    /**
+     * The input buffer does not contain more data.
+     */
+    CX_JSON_NO_DATA,
+    /**
+     * The input ends unexpectedly.
+     *
+     * Refill the buffer with cxJsonFill() to complete the json data.
+     */
+    CX_JSON_INCOMPLETE_DATA,
+    /**
+     * Not used as a status and never returned by any function.
+     *
+     * You can use this enumerator to check for all "good" status results
+     * by checking if the status is less than @c CX_JSON_OK.
+     *
+     * A "good" status means, that you can refill data and continue parsing.
+     */
+    CX_JSON_OK,
+    /**
+     * The input buffer has never been filled.
+     */
+    CX_JSON_NULL_DATA,
+    /**
+     * Allocating memory for the internal buffer failed.
+     */
+    CX_JSON_BUFFER_ALLOC_FAILED,
+    /**
+     * Allocating memory for a json value failed.
+     */
+    CX_JSON_VALUE_ALLOC_FAILED,
+    /**
+     * A number value is incorrectly formatted.
+     */
+    CX_JSON_FORMAT_ERROR_NUMBER,
+    /**
+     * The tokenizer found something unexpected.
+     */
+    CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN
+};
+
+/**
+ * Typedef for the json status enum.
+ */
+typedef enum cx_json_status CxJsonStatus;
+
+/**
+ * The JSON writer settings.
+ */
+struct cx_json_writer_s {
+    /**
+     * Set true to enable pretty output.
+     */
+    bool pretty;
+    /**
+     * Set false to output the members in the order in which they were added.
+     */
+    bool sort_members;
+    /**
+     * The maximum number of fractional digits in a number value.
+     */
+    uint8_t frac_max_digits;
+    /**
+     * Set true to use spaces instead of tab characters.
+     * Indentation is only used in pretty output.
+     */
+    bool indent_space;
+    /**
+     * If @c indent_space is true, this is the number of spaces per tab.
+     * Indentation is only used in pretty output.
+     */
+    uint8_t indent;
+};
+
+/**
+ * Typedef for the json writer.
+ */
+typedef struct cx_json_writer_s CxJsonWriter;
+
+/**
+ * Creates a default writer configuration for compact output.
+ *
+ * @return new JSON writer settings
+ */
+cx_attr_nodiscard
+CxJsonWriter cxJsonWriterCompact(void);
+
+/**
+ * Creates a default writer configuration for pretty output.
+ *
+ * @param use_spaces false if you want tabs, true if you want four spaces instead
+ * @return new JSON writer settings
+ */
+cx_attr_nodiscard
+CxJsonWriter cxJsonWriterPretty(bool use_spaces);
+
+/**
+ * Writes a JSON value to a buffer or stream.
+ *
+ * This function blocks until all data is written or an error when trying
+ * to write data occurs.
+ * The write operation is not atomic in the sense that it might happen
+ * that the data is only partially written when an error occurs with no
+ * way to indicate how much data was written.
+ * To avoid this problem, you can use a CxBuffer as @p target which is
+ * unlikely to fail a write operation and either use the buffer's flush
+ * feature to relay the data or use the data in the buffer manually to
+ * write it to the actual target.
+ *
+ * @param target the buffer or stream where to write to
+ * @param value the value that shall be written
+ * @param wfunc the write function to use
+ * @param settings formatting settings (or @c NULL to use a compact default)
+ * @retval zero success
+ * @retval non-zero when no or not all data could be written
+ */
+cx_attr_nonnull_arg(1, 2, 3)
+int cxJsonWrite(
+    void* target,
+    const CxJsonValue* value,
+    cx_write_func wfunc,
+    const CxJsonWriter* settings
+);
+
+/**
+ * Initializes the json interface.
+ *
+ * @param json the json interface
+ * @param allocator the allocator that shall be used for the produced values
+ * @see cxJsonDestroy()
+ */
+cx_attr_nonnull_arg(1)
+void cxJsonInit(CxJson *json, const CxAllocator *allocator);
+
+/**
+ * Destroys the json interface.
+ *
+ * @param json the json interface
+ * @see cxJsonInit()
+ */
+cx_attr_nonnull
+void cxJsonDestroy(CxJson *json);
+
+/**
+ * Destroys and re-initializes the json interface.
+ *
+ * You might want to use this, to reset the parser after
+ * encountering a syntax error.
+ *
+ * @param json the json interface
+ */
+cx_attr_nonnull
+static inline void cxJsonReset(CxJson *json) {
+    const CxAllocator *allocator = json->allocator;
+    cxJsonDestroy(json);
+    cxJsonInit(json, allocator);
+}
+
+/**
+ * Fills the input buffer.
+ *
+ * @remark The JSON interface tries to avoid copying the input data.
+ * When you use this function and cxJsonNext() interleaving,
+ * no copies are performed. However, you must not free the
+ * pointer to the data in that case. When you invoke the fill
+ * function more than once before calling cxJsonNext(),
+ * the additional data is appended - inevitably leading to
+ * an allocation of a new buffer and copying the previous contents.
+ *
+ * @param json the json interface
+ * @param buf the source buffer
+ * @param len the length of the source buffer
+ * @retval zero success
+ * @retval non-zero internal allocation error
+ * @see cxJsonFill()
+ */
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
+int cxJsonFilln(CxJson *json, const char *buf, size_t len);
+
+#ifdef __cplusplus
+} // extern "C"
+
+cx_attr_nonnull
+static inline int cxJsonFill(
+        CxJson *json,
+        cxstring str
+) {
+    return cxJsonFilln(json, str.ptr, str.length);
+}
+
+cx_attr_nonnull
+static inline int cxJsonFill(
+        CxJson *json,
+        cxmutstr str
+) {
+    return cxJsonFilln(json, str.ptr, str.length);
+}
+
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
+static inline int cxJsonFill(
+        CxJson *json,
+        const char *str
+) {
+    return cxJsonFilln(json, str, strlen(str));
+}
+
+extern "C" {
+#else // __cplusplus
+/**
+ * Fills the input buffer.
+ *
+ * The JSON interface tries to avoid copying the input data.
+ * When you use this function and cxJsonNext() interleaving,
+ * no copies are performed. However, you must not free the
+ * pointer to the data in that case. When you invoke the fill
+ * function more than once before calling cxJsonNext(),
+ * the additional data is appended - inevitably leading to
+ * an allocation of a new buffer and copying the previous contents.
+ *
+ * @param json the json interface
+ * @param str the source string
+ * @retval zero success
+ * @retval non-zero internal allocation error
+ * @see cxJsonFilln()
+ */
+#define cxJsonFill(json, str) _Generic((str), \
+    cxstring: cx_json_fill_cxstr,             \
+    cxmutstr: cx_json_fill_mutstr,            \
+    char*: cx_json_fill_str,                  \
+    const char*: cx_json_fill_str)            \
+    (json, str)
+
+/**
+ * @copydoc cxJsonFill()
+ */
+cx_attr_nonnull
+static inline int cx_json_fill_cxstr(
+        CxJson *json,
+        cxstring str
+) {
+    return cxJsonFilln(json, str.ptr, str.length);
+}
+
+/**
+ * @copydoc cxJsonFill()
+ */
+cx_attr_nonnull
+static inline int cx_json_fill_mutstr(
+        CxJson *json,
+        cxmutstr str
+) {
+    return cxJsonFilln(json, str.ptr, str.length);
+}
+
+/**
+ * @copydoc cxJsonFill()
+ */
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
+static inline int cx_json_fill_str(
+        CxJson *json,
+        const char *str
+) {
+    return cxJsonFilln(json, str, strlen(str));
+}
+#endif
+
+/**
+ * Creates a new (empty) JSON object.
+ *
+ * @param allocator the allocator to use
+ * @return the new JSON object or @c NULL if allocation fails
+ * @see cxJsonObjPutObj()
+ * @see cxJsonArrAddValues()
+ */
+cx_attr_nodiscard
+CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator);
+
+/**
+ * Creates a new (empty) JSON array.
+ *
+ * @param allocator the allocator to use
+ * @return the new JSON array or @c NULL if allocation fails
+ * @see cxJsonObjPutArr()
+ * @see cxJsonArrAddValues()
+ */
+cx_attr_nodiscard
+CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator);
+
+/**
+ * Creates a new JSON number value.
+ *
+ * @param allocator the allocator to use
+ * @param num the numeric value
+ * @return the new JSON value or @c NULL if allocation fails
+ * @see cxJsonObjPutNumber()
+ * @see cxJsonArrAddNumbers()
+ */
+cx_attr_nodiscard
+CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num);
+
+/**
+ * Creates a new JSON number value based on an integer.
+ *
+ * @param allocator the allocator to use
+ * @param num the numeric value
+ * @return the new JSON value or @c NULL if allocation fails
+ * @see cxJsonObjPutInteger()
+ * @see cxJsonArrAddIntegers()
+ */
+cx_attr_nodiscard
+CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num);
+
+/**
+ * Creates a new JSON string.
+ *
+ * @param allocator the allocator to use
+ * @param str the string data
+ * @return the new JSON value or @c NULL if allocation fails
+ * @see cxJsonCreateString()
+ * @see cxJsonObjPutString()
+ * @see cxJsonArrAddStrings()
+ */
+cx_attr_nodiscard
+cx_attr_nonnull_arg(2)
+cx_attr_cstr_arg(2)
+CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char *str);
+
+/**
+ * Creates a new JSON string.
+ *
+ * @param allocator the allocator to use
+ * @param str the string data
+ * @return the new JSON value or @c NULL if allocation fails
+ * @see cxJsonCreateCxString()
+ * @see cxJsonObjPutCxString()
+ * @see cxJsonArrAddCxStrings()
+ */
+cx_attr_nodiscard
+CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str);
+
+/**
+ * Creates a new JSON literal.
+ *
+ * @param allocator the allocator to use
+ * @param lit the type of literal
+ * @return the new JSON value or @c NULL if allocation fails
+ * @see cxJsonObjPutLiteral()
+ * @see cxJsonArrAddLiterals()
+ */
+cx_attr_nodiscard
+CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit);
+
+/**
+ * Adds number values to a JSON array.
+ *
+ * @param arr the JSON array
+ * @param num the array of values
+ * @param count the number of elements
+ * @retval zero success
+ * @retval non-zero allocation failure
+ */
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
+int cxJsonArrAddNumbers(CxJsonValue* arr, const double* num, size_t count);
+
+/**
+ * Adds number values, of which all are integers, to a JSON array.
+ *
+ * @param arr the JSON array
+ * @param num the array of values
+ * @param count the number of elements
+ * @retval zero success
+ * @retval non-zero allocation failure
+ */
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
+int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count);
+
+/**
+ * Adds strings to a JSON array.
+ *
+ * The strings will be copied with the allocator of the array.
+ *
+ * @param arr the JSON array
+ * @param str the array of strings
+ * @param count the number of elements
+ * @retval zero success
+ * @retval non-zero allocation failure
+ * @see cxJsonArrAddCxStrings()
+ */
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
+int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count);
+
+/**
+ * Adds strings to a JSON array.
+ *
+ * The strings will be copied with the allocator of the array.
+ *
+ * @param arr the JSON array
+ * @param str the array of strings
+ * @param count the number of elements
+ * @retval zero success
+ * @retval non-zero allocation failure
+ * @see cxJsonArrAddStrings()
+ */
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
+int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count);
+
+/**
+ * Adds literals to a JSON array.
+ *
+ * @param arr the JSON array
+ * @param lit the array of literal types
+ * @param count the number of elements
+ * @retval zero success
+ * @retval non-zero allocation failure
+ */
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
+int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count);
+
+/**
+ * Add arbitrary values to a JSON array.
+ *
+ * @attention In contrast to all other add functions, this function adds the values
+ * directly to the array instead of copying them.
+ *
+ * @param arr the JSON array
+ * @param val the values
+ * @param count the number of elements
+ * @retval zero success
+ * @retval non-zero allocation failure
+ */
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
+int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count);
+
+/**
+ * Adds or replaces a value within a JSON object.
+ *
+ * The value will be directly added and not copied.
+ *
+ * @note If a value with the specified @p name already exists,
+ * it will be (recursively) freed with its own allocator.
+ *
+ * @param obj the JSON object
+ * @param name the name of the value
+ * @param child the value
+ * @retval zero success
+ * @retval non-zero allocation failure
+ */
+cx_attr_nonnull
+int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child);
+
+/**
+ * Creates a new JSON object and adds it to an existing object.
+ *
+ * @param obj the target JSON object
+ * @param name the name of the new value
+ * @return the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateObj()
+ */
+cx_attr_nonnull
+CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name);
+
+/**
+ * Creates a new JSON array and adds it to an object.
+ *
+ * @param obj the target JSON object
+ * @param name the name of the new value
+ * @return the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateArr()
+ */
+cx_attr_nonnull
+CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name);
+
+/**
+ * Creates a new JSON number and adds it to an object.
+ *
+ * @param obj the target JSON object
+ * @param name the name of the new value
+ * @param num the numeric value
+ * @return the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateNumber()
+ */
+cx_attr_nonnull
+CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num);
+
+/**
+ * Creates a new JSON number, based on an integer, and adds it to an object.
+ *
+ * @param obj the target JSON object
+ * @param name the name of the new value
+ * @param num the numeric value
+ * @return the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateInteger()
+ */
+cx_attr_nonnull
+CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num);
+
+/**
+ * Creates a new JSON string and adds it to an object.
+ *
+ * The string data is copied.
+ *
+ * @param obj the target JSON object
+ * @param name the name of the new value
+ * @param str the string data
+ * @return the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateString()
+ */
+cx_attr_nonnull
+cx_attr_cstr_arg(3)
+CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str);
+
+/**
+ * Creates a new JSON string and adds it to an object.
+ *
+ * The string data is copied.
+ *
+ * @param obj the target JSON object
+ * @param name the name of the new value
+ * @param str the string data
+ * @return the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateCxString()
+ */
+cx_attr_nonnull
+CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str);
+
+/**
+ * Creates a new JSON literal and adds it to an object.
+ *
+ * @param obj the target JSON object
+ * @param name the name of the new value
+ * @param lit the type of literal
+ * @return the new value or @c NULL if allocation fails
+ * @see cxJsonObjPut()
+ * @see cxJsonCreateLiteral()
+ */
+cx_attr_nonnull
+CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
+
+/**
+ * Recursively deallocates the memory of a JSON value.
+ *
+ * @remark The type of each deallocated value will be changed
+ * to #CX_JSON_NOTHING and values of such type will be skipped
+ * by the de-allocation. That means, this function protects
+ * you from double-frees when you are accidentally freeing
+ * a nested value and then the parent value (or vice versa).
+ *
+ * @param value the value
+ */
+void cxJsonValueFree(CxJsonValue *value);
+
+/**
+ * Tries to obtain the next JSON value.
+ *
+ * Before this function can be called, the input buffer needs
+ * to be filled with cxJsonFill().
+ *
+ * When this function returns #CX_JSON_INCOMPLETE_DATA, you can
+ * add the missing data with another invocation of cxJsonFill()
+ * and then repeat the call to cxJsonNext().
+ *
+ * @param json the json interface
+ * @param value a pointer where the next value shall be stored
+ * @retval CX_JSON_NO_ERROR successfully retrieve the @p value
+ * @retval CX_JSON_NO_DATA there is no (more) data in the buffer to read from
+ * @retval CX_JSON_INCOMPLETE_DATA an incomplete value was read
+ * and more data needs to be filled
+ * @retval CX_JSON_NULL_DATA the buffer was never initialized
+ * @retval CX_JSON_BUFFER_ALLOC_FAILED allocating internal buffer space failed
+ * @retval CX_JSON_VALUE_ALLOC_FAILED allocating memory for a CxJsonValue failed
+ * @retval CX_JSON_FORMAT_ERROR_NUMBER the JSON text contains an illegally formatted number
+ * @retval CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN JSON syntax error
+ */
+cx_attr_nonnull
+cx_attr_access_w(2)
+CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value);
+
+/**
+ * Checks if the specified value is a JSON object.
+ *
+ * @param value a pointer to the value
+ * @retval true the value is a JSON object
+ * @retval false otherwise
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsObject(const CxJsonValue *value) {
+    return value->type == CX_JSON_OBJECT;
+}
+
+/**
+ * Checks if the specified value is a JSON array.
+ *
+ * @param value a pointer to the value
+ * @retval true the value is a JSON array
+ * @retval false otherwise
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsArray(const CxJsonValue *value) {
+    return value->type == CX_JSON_ARRAY;
+}
+
+/**
+ * Checks if the specified value is a string.
+ *
+ * @param value a pointer to the value
+ * @retval true the value is a string
+ * @retval false otherwise
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsString(const CxJsonValue *value) {
+    return value->type == CX_JSON_STRING;
+}
+
+/**
+ * Checks if the specified value is a JSON number.
+ *
+ * This function will return true for both floating point and
+ * integer numbers.
+ *
+ * @param value a pointer to the value
+ * @retval true the value is a JSON number
+ * @retval false otherwise
+ * @see cxJsonIsInteger()
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsNumber(const CxJsonValue *value) {
+    return value->type == CX_JSON_NUMBER || value->type == CX_JSON_INTEGER;
+}
+
+/**
+ * Checks if the specified value is an integer number.
+ *
+ * @param value a pointer to the value
+ * @retval true the value is an integer number
+ * @retval false otherwise
+ * @see cxJsonIsNumber()
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsInteger(const CxJsonValue *value) {
+    return value->type == CX_JSON_INTEGER;
+}
+
+/**
+ * Checks if the specified value is a JSON literal.
+ *
+ * JSON literals are @c true, @c false, and @c null.
+ *
+ * @param value a pointer to the value
+ * @retval true the value is a JSON literal
+ * @retval false otherwise
+ * @see cxJsonIsTrue()
+ * @see cxJsonIsFalse()
+ * @see cxJsonIsNull()
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsLiteral(const CxJsonValue *value) {
+    return value->type == CX_JSON_LITERAL;
+}
+
+/**
+ * Checks if the specified value is a Boolean literal.
+ *
+ * @param value a pointer to the value
+ * @retval true the value is either @c true or @c false
+ * @retval false otherwise
+ * @see cxJsonIsTrue()
+ * @see cxJsonIsFalse()
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsBool(const CxJsonValue *value) {
+    return cxJsonIsLiteral(value) && value->value.literal != CX_JSON_NULL;
+}
+
+/**
+ * Checks if the specified value is @c true.
+ *
+ * @remark Be advised, that this is not the same as
+ * testing @c !cxJsonIsFalse(v).
+ *
+ * @param value a pointer to the value
+ * @retval true the value is @c true
+ * @retval false otherwise
+ * @see cxJsonIsBool()
+ * @see cxJsonIsFalse()
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsTrue(const CxJsonValue *value) {
+    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_TRUE;
+}
+
+/**
+ * Checks if the specified value is @c false.
+ *
+ * @remark Be advised, that this is not the same as
+ * testing @c !cxJsonIsTrue(v).
+ *
+ * @param value a pointer to the value
+ * @retval true the value is @c false
+ * @retval false otherwise
+ * @see cxJsonIsBool()
+ * @see cxJsonIsTrue()
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsFalse(const CxJsonValue *value) {
+    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_FALSE;
+}
+
+/**
+ * Checks if the specified value is @c null.
+ *
+ * @param value a pointer to the value
+ * @retval true the value is @c null
+ * @retval false otherwise
+ * @see cxJsonIsLiteral()
+ */
+cx_attr_nonnull
+static inline bool cxJsonIsNull(const CxJsonValue *value) {
+    return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_NULL;
+}
+
+/**
+ * Obtains a C string from the given JSON value.
+ *
+ * If the @p value is not a string, the behavior is undefined.
+ *
+ * @param value the JSON value
+ * @return the value represented as C string
+ * @see cxJsonIsString()
+ */
+cx_attr_nonnull
+cx_attr_returns_nonnull
+static inline char *cxJsonAsString(const CxJsonValue *value) {
+    return value->value.string.ptr;
+}
+
+/**
+ * Obtains a UCX string from the given JSON value.
+ *
+ * If the @p value is not a string, the behavior is undefined.
+ *
+ * @param value the JSON value
+ * @return the value represented as UCX string
+ * @see cxJsonIsString()
+ */
+cx_attr_nonnull
+static inline cxstring cxJsonAsCxString(const CxJsonValue *value) {
+    return cx_strcast(value->value.string);
+}
+
+/**
+ * Obtains a mutable UCX string from the given JSON value.
+ *
+ * If the @p value is not a string, the behavior is undefined.
+ *
+ * @param value the JSON value
+ * @return the value represented as mutable UCX string
+ * @see cxJsonIsString()
+ */
+cx_attr_nonnull
+static inline cxmutstr cxJsonAsCxMutStr(const CxJsonValue *value) {
+    return value->value.string;
+}
+
+/**
+ * Obtains a double-precision floating point value from the given JSON value.
+ *
+ * If the @p value is not a JSON number, the behavior is undefined.
+ *
+ * @param value the JSON value
+ * @return the value represented as double
+ * @see cxJsonIsNumber()
+ */
+cx_attr_nonnull
+static inline double cxJsonAsDouble(const CxJsonValue *value) {
+    if (value->type == CX_JSON_INTEGER) {
+        return (double) value->value.integer;
+    } else {
+        return value->value.number;
+    }
+}
+
+/**
+ * Obtains a 64-bit signed integer from the given JSON value.
+ *
+ * If the @p value is not a JSON number, the behavior is undefined.
+ * If it is a JSON number, but not an integer, the value will be
+ * converted to an integer, possibly losing precision.
+ *
+ * @param value the JSON value
+ * @return the value represented as double
+ * @see cxJsonIsNumber()
+ * @see cxJsonIsInteger()
+ */
+cx_attr_nonnull
+static inline int64_t cxJsonAsInteger(const CxJsonValue *value) {
+    if (value->type == CX_JSON_INTEGER) {
+        return value->value.integer;
+    } else {
+        return (int64_t) value->value.number;
+    }
+}
+
+/**
+ * Obtains a Boolean value from the given JSON value.
+ *
+ * If the @p value is not a JSON literal, the behavior is undefined.
+ * The @c null literal is interpreted as @c false.
+ *
+ * @param value the JSON value
+ * @return the value represented as double
+ * @see cxJsonIsLiteral()
+ */
+cx_attr_nonnull
+static inline bool cxJsonAsBool(const CxJsonValue *value) {
+    return value->value.literal == CX_JSON_TRUE;
+}
+
+/**
+ * Returns the size of a JSON array.
+ *
+ * If the @p value is not a JSON array, the behavior is undefined.
+ *
+ * @param value the JSON value
+ * @return the size of the array
+ * @see cxJsonIsArray()
+ */
+cx_attr_nonnull
+static inline size_t cxJsonArrSize(const CxJsonValue *value) {
+    return value->value.array.array_size;
+}
+
+/**
+ * Returns an element from a JSON array.
+ *
+ * If the @p value is not a JSON array, the behavior is undefined.
+ *
+ * This function guarantees to return a value. If the index is
+ * out of bounds, the returned value will be of type
+ * #CX_JSON_NOTHING, but never @c NULL.
+ *
+ * @param value the JSON value
+ * @param index the index in the array
+ * @return the value at the specified index
+ * @see cxJsonIsArray()
+ */
+cx_attr_nonnull
+cx_attr_returns_nonnull
+CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index);
+
+/**
+ * Returns an iterator over the JSON array elements.
+ *
+ * The iterator yields values of type @c CxJsonValue* .
+ *
+ * If the @p value is not a JSON array, the behavior is undefined.
+ *
+ * @param value the JSON value
+ * @return an iterator over the array elements
+ * @see cxJsonIsArray()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+CxIterator cxJsonArrIter(const CxJsonValue *value);
+
+/**
+ * Returns an iterator over the JSON object members.
+ *
+ * The iterator yields values of type @c CxJsonObjValue* which
+ * contain the name and value of the member.
+ *
+ * If the @p value is not a JSON object, the behavior is undefined.
+ *
+ * @param value the JSON value
+ * @return an iterator over the object members
+ * @see cxJsonIsObject()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+CxIterator cxJsonObjIter(const CxJsonValue *value);
+
+/**
+ * @copydoc cxJsonObjGet()
+ */
+cx_attr_nonnull
+cx_attr_returns_nonnull
+CxJsonValue *cx_json_obj_get_cxstr(const CxJsonValue *value, cxstring name);
+
+#ifdef __cplusplus
+} // extern "C"
+
+CxJsonValue *cxJsonObjGet(const CxJsonValue *value, cxstring name) {
+    return cx_json_obj_get_cxstr(value, name);
+}
+
+CxJsonValue *cxJsonObjGet(const CxJsonValue *value, cxmutstr name) {
+    return cx_json_obj_get_cxstr(value, cx_strcast(name));
+}
+
+CxJsonValue *cxJsonObjGet(const CxJsonValue *value, const char *name) {
+    return cx_json_obj_get_cxstr(value, cx_str(name));
+}
+
+extern "C" {
+#else
+/**
+ * Returns a value corresponding to a key in a JSON object.
+ *
+ * If the @p value is not a JSON object, the behavior is undefined.
+ *
+ * This function guarantees to return a JSON value. If the
+ * object does not contain @p name, the returned JSON value
+ * will be of type #CX_JSON_NOTHING, but never @c NULL.
+ *
+ * @param value the JSON object
+ * @param name the key to look up
+ * @return the value corresponding to the key
+ * @see cxJsonIsObject()
+ */
+#define cxJsonObjGet(value, name) _Generic((name), \
+        cxstring: cx_json_obj_get_cxstr,           \
+        cxmutstr: cx_json_obj_get_mutstr,          \
+        char*: cx_json_obj_get_str,                \
+        const char*: cx_json_obj_get_str)          \
+        (value, name)
+
+/**
+ * @copydoc cxJsonObjGet()
+ */
+cx_attr_nonnull
+cx_attr_returns_nonnull
+static inline CxJsonValue *cx_json_obj_get_mutstr(const CxJsonValue *value, cxmutstr name) {
+    return cx_json_obj_get_cxstr(value, cx_strcast(name));
+}
+
+/**
+ * @copydoc cxJsonObjGet()
+ */
+cx_attr_nonnull
+cx_attr_returns_nonnull
+cx_attr_cstr_arg(2)
+static inline CxJsonValue *cx_json_obj_get_str(const CxJsonValue *value, const char *name) {
+    return cx_json_obj_get_cxstr(value, cx_str(name));
+}
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UCX_JSON_H */
+
--- a/ucx/cx/linked_list.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/linked_list.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,12 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file linked_list.h
- * \brief Linked list implementation.
- * \details Also provides several low-level functions for custom linked list implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file linked_list.h
+ * @brief Linked list implementation.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_LINKED_LIST_H
@@ -46,24 +45,28 @@
 
 /**
  * The maximum item size that uses SBO swap instead of relinking.
+ *
  */
-extern unsigned cx_linked_list_swap_sbo_size;
+extern const unsigned cx_linked_list_swap_sbo_size;
 
 /**
- * Allocates a linked list for storing elements with \p elem_size bytes each.
+ * Allocates a linked list for storing elements with @p elem_size bytes each.
  *
- * If \p elem_size is CX_STORE_POINTERS, the created list will be created as if
+ * If @p elem_size is CX_STORE_POINTERS, the created list will be created as if
  * cxListStorePointers() was called immediately after creation and the compare
  * function will be automatically set to cx_cmp_ptr(), if none is given.
  *
  * @param allocator the allocator for allocating the list nodes
- * (if \c NULL the cxDefaultAllocator will be used)
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param comparator the comparator for the elements
- * (if \c NULL, and the list is not storing pointers, sort and find
+ * (if @c NULL, and the list is not storing pointers, sort and find
  * functions will not work)
  * @param elem_size the size of each element in bytes
  * @return the created list
  */
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxListFree, 1)
 CxList *cxLinkedListCreate(
         const CxAllocator *allocator,
         cx_compare_func comparator,
@@ -71,18 +74,18 @@
 );
 
 /**
- * Allocates a linked list for storing elements with \p elem_size bytes each.
+ * Allocates a linked list for storing elements with @p elem_size bytes each.
  *
  * The list will use cxDefaultAllocator and no comparator function. If you want
  * to call functions that need a comparator, you must either set one immediately
  * after list creation or use cxLinkedListCreate().
  *
- * If \p elem_size is CX_STORE_POINTERS, the created list will be created as if
+ * If @p elem_size is CX_STORE_POINTERS, the created list will be created as if
  * cxListStorePointers() was called immediately after creation and the compare
  * function will be automatically set to cx_cmp_ptr().
  *
- * @param elem_size the size of each element in bytes
- * @return the created list
+ * @param elem_size (@c size_t) the size of each element in bytes
+ * @return (@c CxList*) the created list
  */
 #define cxLinkedListCreateSimple(elem_size) \
     cxLinkedListCreate(NULL, NULL, elem_size)
@@ -91,11 +94,11 @@
  * Finds the node at a certain index.
  *
  * This function can be used to start at an arbitrary position within the list.
- * If the search index is large than the start index, \p loc_advance must denote
- * the location of some sort of \c next pointer (i.e. a pointer to the next node).
+ * If the search index is large than the start index, @p loc_advance must denote
+ * the location of some sort of @c next pointer (i.e. a pointer to the next node).
  * But it is also possible that the search index is smaller than the start index
  * (e.g. in cases where traversing a list backwards is faster) in which case
- * \p loc_advance must denote the location of some sort of \c prev pointer
+ * @p loc_advance must denote the location of some sort of @c prev pointer
  * (i.e. a pointer to the previous node).
  *
  * @param start a pointer to the start node
@@ -104,7 +107,8 @@
  * @param index the search index
  * @return the node found at the specified index
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 void *cx_linked_list_at(
         const void *start,
         size_t start_index,
@@ -117,12 +121,12 @@
  *
  * @param start a pointer to the start node
  * @param loc_advance the location of the pointer to advance
- * @param loc_data the location of the \c data pointer within your node struct
- * @param cmp_func a compare function to compare \p elem against the node data
+ * @param loc_data the location of the @c data pointer within your node struct
+ * @param cmp_func a compare function to compare @p elem against the node data
  * @param elem a pointer to the element to find
  * @return the index of the element or a negative value if it could not be found
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 ssize_t cx_linked_list_find(
         const void *start,
         ptrdiff_t loc_advance,
@@ -134,16 +138,16 @@
 /**
  * Finds the node containing an element within a linked list.
  *
- * @param result a pointer to the memory where the node pointer (or \c NULL if the element
+ * @param result a pointer to the memory where the node pointer (or @c NULL if the element
  * could not be found) shall be stored to
  * @param start a pointer to the start node
  * @param loc_advance the location of the pointer to advance
- * @param loc_data the location of the \c data pointer within your node struct
- * @param cmp_func a compare function to compare \p elem against the node data
+ * @param loc_data the location of the @c data pointer within your node struct
+ * @param cmp_func a compare function to compare @p elem against the node data
  * @param elem a pointer to the element to find
  * @return the index of the element or a negative value if it could not be found
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 ssize_t cx_linked_list_find_node(
         void **result,
         const void *start,
@@ -156,15 +160,16 @@
 /**
  * Finds the first node in a linked list.
  *
- * The function starts with the pointer denoted by \p node and traverses the list
+ * The function starts with the pointer denoted by @p node and traverses the list
  * along a prev pointer whose location within the node struct is
- * denoted by \p loc_prev.
+ * denoted by @p loc_prev.
  *
  * @param node a pointer to a node in the list
- * @param loc_prev the location of the \c prev pointer
+ * @param loc_prev the location of the @c prev pointer
  * @return a pointer to the first node
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_returns_nonnull
 void *cx_linked_list_first(
         const void *node,
         ptrdiff_t loc_prev
@@ -173,15 +178,16 @@
 /**
  * Finds the last node in a linked list.
  *
- * The function starts with the pointer denoted by \p node and traverses the list
+ * The function starts with the pointer denoted by @p node and traverses the list
  * along a next pointer whose location within the node struct is
- * denoted by \p loc_next.
+ * denoted by @p loc_next.
  *
  * @param node a pointer to a node in the list
- * @param loc_next the location of the \c next pointer
+ * @param loc_next the location of the @c next pointer
  * @return a pointer to the last node
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_returns_nonnull
 void *cx_linked_list_last(
         const void *node,
         ptrdiff_t loc_next
@@ -190,14 +196,14 @@
 /**
  * Finds the predecessor of a node in case it is not linked.
  *
- * \remark If \p node is not contained in the list starting with \p begin, the behavior is undefined.
+ * @remark If @p node is not contained in the list starting with @p begin, the behavior is undefined.
  *
  * @param begin the node where to start the search
- * @param loc_next the location of the \c next pointer
+ * @param loc_next the location of the @c next pointer
  * @param node the successor of the node to find
- * @return the node or \c NULL if \p node has no predecessor
+ * @return the node or @c NULL if @p node has no predecessor
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void *cx_linked_list_prev(
         const void *begin,
         ptrdiff_t loc_next,
@@ -208,15 +214,15 @@
  * Adds a new node to a linked list.
  * The node must not be part of any list already.
  *
- * \remark One of the pointers \p begin or \p end may be \c NULL, but not both.
+ * @remark One of the pointers @p begin or @p end may be @c NULL, but not both.
  *
- * @param begin a pointer to the begin node pointer (if your list has one)
+ * @param begin a pointer to the beginning node pointer (if your list has one)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param new_node a pointer to the node that shall be appended
  */
-__attribute__((__nonnull__(5)))
+cx_attr_nonnull_arg(5)
 void cx_linked_list_add(
         void **begin,
         void **end,
@@ -229,15 +235,15 @@
  * Prepends a new node to a linked list.
  * The node must not be part of any list already.
  *
- * \remark One of the pointers \p begin or \p end may be \c NULL, but not both.
+ * @remark One of the pointers @p begin or @p end may be @c NULL, but not both.
  *
- * @param begin a pointer to the begin node pointer (if your list has one)
+ * @param begin a pointer to the beginning node pointer (if your list has one)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param new_node a pointer to the node that shall be prepended
  */
-__attribute__((__nonnull__(5)))
+cx_attr_nonnull_arg(5)
 void cx_linked_list_prepend(
         void **begin,
         void **end,
@@ -249,12 +255,12 @@
 /**
  * Links two nodes.
  *
- * @param left the new predecessor of \p right
- * @param right the new successor of \p left
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param left the new predecessor of @p right
+ * @param right the new successor of @p left
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_linked_list_link(
         void *left,
         void *right,
@@ -267,12 +273,12 @@
  *
  * If right is not the successor of left, the behavior is undefined.
  *
- * @param left the predecessor of \p right
- * @param right the successor of \p left
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param left the predecessor of @p right
+ * @param right the successor of @p left
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_linked_list_unlink(
         void *left,
         void *right,
@@ -284,17 +290,17 @@
  * Inserts a new node after a given node of a linked list.
  * The new node must not be part of any list already.
  *
- * \note If you specify \c NULL as the \p node to insert after, this function needs either the \p begin or
- * the \p end pointer to determine the start of the list. Then the new node will be prepended to the list.
+ * @note If you specify @c NULL as the @p node to insert after, this function needs either the @p begin or
+ * the @p end pointer to determine the start of the list. Then the new node will be prepended to the list.
  *
- * @param begin a pointer to the begin node pointer (if your list has one)
+ * @param begin a pointer to the beginning node pointer (if your list has one)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
- * @param node the node after which to insert (\c NULL if you want to prepend the node to the list)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param node the node after which to insert (@c NULL if you want to prepend the node to the list)
  * @param new_node a pointer to the node that shall be inserted
  */
-__attribute__((__nonnull__(6)))
+cx_attr_nonnull_arg(6)
 void cx_linked_list_insert(
         void **begin,
         void **end,
@@ -309,22 +315,22 @@
  * The chain must not be part of any list already.
  *
  * If you do not explicitly specify the end of the chain, it will be determined by traversing
- * the \c next pointer.
+ * the @c next pointer.
  *
- * \note If you specify \c NULL as the \p node to insert after, this function needs either the \p begin or
- * the \p end pointer to determine the start of the list. If only the \p end pointer is specified, you also need
- * to provide a valid \p loc_prev location.
+ * @note If you specify @c NULL as the @p node to insert after, this function needs either the @p begin or
+ * the @p end pointer to determine the start of the list. If only the @p end pointer is specified, you also need
+ * to provide a valid @p loc_prev location.
  * Then the chain will be prepended to the list.
  *
- * @param begin a pointer to the begin node pointer (if your list has one)
+ * @param begin a pointer to the beginning node pointer (if your list has one)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
- * @param node the node after which to insert (\c NULL to prepend the chain to the list)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param node the node after which to insert (@c NULL to prepend the chain to the list)
  * @param insert_begin a pointer to the first node of the chain that shall be inserted
  * @param insert_end a pointer to the last node of the chain (or NULL if the last node shall be determined)
  */
-__attribute__((__nonnull__(6)))
+cx_attr_nonnull_arg(6)
 void cx_linked_list_insert_chain(
         void **begin,
         void **end,
@@ -339,17 +345,17 @@
  * Inserts a node into a sorted linked list.
  * The new node must not be part of any list already.
  *
- * If the list starting with the node pointed to by \p begin is not sorted
+ * If the list starting with the node pointed to by @p begin is not sorted
  * already, the behavior is undefined.
  *
- * @param begin a pointer to the begin node pointer (required)
+ * @param begin a pointer to the beginning node pointer (required)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param new_node a pointer to the node that shall be inserted
  * @param cmp_func a compare function that will receive the node pointers
  */
-__attribute__((__nonnull__(1, 5, 6)))
+cx_attr_nonnull_arg(1, 5, 6)
 void cx_linked_list_insert_sorted(
         void **begin,
         void **end,
@@ -363,22 +369,22 @@
  * Inserts a chain of nodes into a sorted linked list.
  * The chain must not be part of any list already.
  *
- * If either the list starting with the node pointed to by \p begin or the list
- * starting with \p insert_begin is not sorted, the behavior is undefined.
+ * If either the list starting with the node pointed to by @p begin or the list
+ * starting with @p insert_begin is not sorted, the behavior is undefined.
  *
- * \attention In contrast to cx_linked_list_insert_chain(), the source chain
+ * @attention In contrast to cx_linked_list_insert_chain(), the source chain
  * will be broken and inserted into the target list so that the resulting list
- * will be sorted according to \p cmp_func. That means, each node in the source
+ * will be sorted according to @p cmp_func. That means, each node in the source
  * chain may be re-linked with nodes from the target list.
  *
- * @param begin a pointer to the begin node pointer (required)
+ * @param begin a pointer to the beginning node pointer (required)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param insert_begin a pointer to the first node of the chain that shall be inserted
  * @param cmp_func a compare function that will receive the node pointers
  */
-__attribute__((__nonnull__(1, 5, 6)))
+cx_attr_nonnull_arg(1, 5, 6)
 void cx_linked_list_insert_sorted_chain(
         void **begin,
         void **end,
@@ -389,39 +395,72 @@
 );
 
 /**
- * Removes a node from the linked list.
+ * Removes a chain of nodes from the linked list.
  *
- * If the node to remove is the begin (resp. end) node of the list and if \p begin (resp. \p end)
+ * If one of the nodes to remove is the beginning (resp. end) node of the list and if @p begin (resp. @p end)
  * addresses are provided, the pointers are adjusted accordingly.
  *
  * The following combinations of arguments are valid (more arguments are optional):
- * \li \p loc_next and \p loc_prev (ancestor node is determined by using the prev pointer, overall O(1) performance)
- * \li \p loc_next and \p begin (ancestor node is determined by list traversal, overall O(n) performance)
+ * @li @p loc_next and @p loc_prev (ancestor node is determined by using the prev pointer, overall O(1) performance)
+ * @li @p loc_next and @p begin (ancestor node is determined by list traversal, overall O(n) performance)
+ *
+ * @remark The @c next and @c prev pointers of the removed node are not cleared by this function and may still be used
+ * to traverse to a former adjacent node in the list, or within the chain.
  *
- * \remark The \c next and \c prev pointers of the removed node are not cleared by this function and may still be used
+ * @param begin a pointer to the beginning node pointer (optional)
+ * @param end a pointer to the end node pointer (optional)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param node the start node of the chain
+ * @param num the number of nodes to remove
+ * @return the actual number of nodes that were removed (can be less when the list did not have enough nodes)
+ */
+cx_attr_nonnull_arg(5)
+size_t cx_linked_list_remove_chain(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *node,
+        size_t num
+);
+
+/**
+ * Removes a node from the linked list.
+ *
+ * If the node to remove is the beginning (resp. end) node of the list and if @p begin (resp. @p end)
+ * addresses are provided, the pointers are adjusted accordingly.
+ *
+ * The following combinations of arguments are valid (more arguments are optional):
+ * @li @p loc_next and @p loc_prev (ancestor node is determined by using the prev pointer, overall O(1) performance)
+ * @li @p loc_next and @p begin (ancestor node is determined by list traversal, overall O(n) performance)
+ *
+ * @remark The @c next and @c prev pointers of the removed node are not cleared by this function and may still be used
  * to traverse to a former adjacent node in the list.
  *
- * @param begin a pointer to the begin node pointer (optional)
+ * @param begin a pointer to the beginning node pointer (optional)
  * @param end a pointer to the end node pointer (optional)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param node the node to remove
  */
-__attribute__((__nonnull__(5)))
-void cx_linked_list_remove(
+cx_attr_nonnull_arg(5)
+static inline void cx_linked_list_remove(
         void **begin,
         void **end,
         ptrdiff_t loc_prev,
         ptrdiff_t loc_next,
         void *node
-);
-
+) {
+    cx_linked_list_remove_chain(begin, end, loc_prev, loc_next, node, 1);
+}
 
 /**
- * Determines the size of a linked list starting with \p node.
+ * Determines the size of a linked list starting with @p node.
+ *
  * @param node the first node
- * @param loc_next the location of the \c next pointer within the node struct
- * @return the size of the list or zero if \p node is \c NULL
+ * @param loc_next the location of the @c next pointer within the node struct
+ * @return the size of the list or zero if @p node is @c NULL
  */
 size_t cx_linked_list_size(
         const void *node,
@@ -432,25 +471,25 @@
  * Sorts a linked list based on a comparison function.
  *
  * This function can work with linked lists of the following structure:
- * \code
+ * @code
  * typedef struct node node;
  * struct node {
  *   node* prev;
  *   node* next;
  *   my_payload data;
  * }
- * \endcode
+ * @endcode
  *
  * @note This is a recursive function with at most logarithmic recursion depth.
  *
- * @param begin a pointer to the begin node pointer (required)
+ * @param begin a pointer to the beginning node pointer (required)
  * @param end a pointer to the end node pointer (optional)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if not present)
- * @param loc_next the location of a \c next pointer within your node struct (required)
- * @param loc_data the location of the \c data pointer within your node struct
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if not present)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param loc_data the location of the @c data pointer within your node struct
  * @param cmp_func the compare function defining the sort order
  */
-__attribute__((__nonnull__(1, 6)))
+cx_attr_nonnull_arg(1, 6)
 void cx_linked_list_sort(
         void **begin,
         void **end,
@@ -464,17 +503,17 @@
 /**
  * Compares two lists element wise.
  *
- * \note Both list must have the same structure.
+ * @attention Both list must have the same structure.
  *
- * @param begin_left the begin of the left list (\c NULL denotes an empty list)
- * @param begin_right the begin of the right list  (\c NULL denotes an empty list)
+ * @param begin_left the beginning of the left list (@c NULL denotes an empty list)
+ * @param begin_right the beginning of the right list  (@c NULL denotes an empty list)
  * @param loc_advance the location of the pointer to advance
- * @param loc_data the location of the \c data pointer within your node struct
+ * @param loc_data the location of the @c data pointer within your node struct
  * @param cmp_func the function to compare the elements
- * @return the first non-zero result of invoking \p cmp_func or: negative if the left list is smaller than the
+ * @return the first non-zero result of invoking @p cmp_func or: negative if the left list is smaller than the
  * right list, positive if the left list is larger than the right list, zero if both lists are equal.
  */
-__attribute__((__nonnull__(5)))
+cx_attr_nonnull_arg(5)
 int cx_linked_list_compare(
         const void *begin_left,
         const void *begin_right,
@@ -486,12 +525,12 @@
 /**
  * Reverses the order of the nodes in a linked list.
  *
- * @param begin a pointer to the begin node pointer (required)
+ * @param begin a pointer to the beginning node pointer (required)
  * @param end a pointer to the end node pointer (optional)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  */
-__attribute__((__nonnull__(1)))
+cx_attr_nonnull_arg(1)
 void cx_linked_list_reverse(
         void **begin,
         void **end,
--- a/ucx/cx/list.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/list.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file list.h
- * \brief Interface for list implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file list.h
+ * @brief Interface for list implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_LIST_H
@@ -52,6 +52,9 @@
  * Structure for holding the base data of a list.
  */
 struct cx_list_s {
+    /**
+     * Common members for collections.
+     */
     CX_COLLECTION_BASE;
     /**
      * The list class definition.
@@ -71,9 +74,9 @@
      * Destructor function.
      *
      * Implementations SHALL invoke the content destructor functions if provided
-     * and SHALL deallocate the list memory.
+     * and SHALL deallocate the entire list memory.
      */
-    void (*destructor)(struct cx_list_s *list);
+    void (*deallocate)(struct cx_list_s *list);
 
     /**
      * Member function for inserting a single element.
@@ -88,6 +91,7 @@
     /**
      * Member function for inserting multiple elements.
      * Implementors SHOULD see to performant implementations for corner cases.
+     *
      * @see cx_list_default_insert_array()
      */
     size_t (*insert_array)(
@@ -118,11 +122,20 @@
     );
 
     /**
-     * Member function for removing an element.
+     * Member function for removing elements.
+     *
+     * Implementations SHALL check if @p targetbuf is set and copy the elements
+     * to the buffer without invoking any destructor.
+     * When @p targetbuf is not set, the destructors SHALL be invoked.
+     *
+     * The function SHALL return the actual number of elements removed, which
+     * might be lower than @p num when going out of bounds.
      */
-    int (*remove)(
+    size_t (*remove)(
             struct cx_list_s *list,
-            size_t index
+            size_t index,
+            size_t num,
+            void *targetbuf
     );
 
     /**
@@ -132,6 +145,7 @@
 
     /**
      * Member function for swapping two elements.
+     *
      * @see cx_list_default_swap()
      */
     int (*swap)(
@@ -158,7 +172,8 @@
     );
 
     /**
-     * Member function for sorting the list in-place.
+     * Member function for sorting the list.
+     *
      * @see cx_list_default_sort()
      */
     void (*sort)(struct cx_list_s *list);
@@ -166,8 +181,9 @@
     /**
      * Optional member function for comparing this list
      * to another list of the same type.
-     * If set to \c NULL, comparison won't be optimized.
+     * If set to @c NULL, comparison won't be optimized.
      */
+    cx_attr_nonnull
     int (*compare)(
             const struct cx_list_s *list,
             const struct cx_list_s *other
@@ -202,7 +218,7 @@
  * @param n the number of elements to insert
  * @return the number of elements actually inserted
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cx_list_default_insert_array(
         struct cx_list_s *list,
         size_t index,
@@ -216,7 +232,7 @@
  * This function uses the array insert function to insert consecutive groups
  * of sorted data.
  *
- * The source data \em must already be sorted wrt. the list's compare function.
+ * The source data @em must already be sorted wrt. the list's compare function.
  *
  * Use this in your own list class if you do not want to implement an optimized
  * version for your list.
@@ -226,7 +242,7 @@
  * @param n the number of elements to insert
  * @return the number of elements actually inserted
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cx_list_default_insert_sorted(
         struct cx_list_s *list,
         const void *sorted_data,
@@ -244,7 +260,7 @@
  *
  * @param list the list that shall be sorted
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_list_default_sort(struct cx_list_s *list);
 
 /**
@@ -256,10 +272,11 @@
  * @param list the list in which to swap
  * @param i index of one element
  * @param j index of the other element
- * @return zero on success, non-zero when indices are out of bounds or memory
+ * @retval zero success
+ * @retval non-zero when indices are out of bounds or memory
  * allocation for the temporary buffer fails
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cx_list_default_swap(struct cx_list_s *list, size_t i, size_t j);
 
 /**
@@ -276,7 +293,7 @@
  * @param list the list
  * @see cxListStorePointers()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxListStoreObjects(CxList *list);
 
 /**
@@ -291,7 +308,7 @@
  * @param list the list
  * @see cxListStoreObjects()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxListStorePointers(CxList *list);
 
 /**
@@ -301,7 +318,7 @@
  * @return true, if this list is storing pointers
  * @see cxListStorePointers()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxListIsStoringPointers(const CxList *list) {
     return list->collection.store_pointer;
 }
@@ -312,7 +329,7 @@
  * @param list the list
  * @return the number of currently stored elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListSize(const CxList *list) {
     return list->collection.size;
 }
@@ -322,10 +339,11 @@
  *
  * @param list the list
  * @param elem a pointer to the element to add
- * @return zero on success, non-zero on memory allocation failure
+ * @retval zero success
+ * @retval non-zero memory allocation failure
  * @see cxListAddArray()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListAdd(
         CxList *list,
         const void *elem
@@ -339,9 +357,9 @@
  * This method is more efficient than invoking cxListAdd() multiple times.
  *
  * If there is not enough memory to add all elements, the returned value is
- * less than \p n.
+ * less than @p n.
  *
- * If this list is storing pointers instead of objects \p array is expected to
+ * If this list is storing pointers instead of objects @p array is expected to
  * be an array of pointers.
  *
  * @param list the list
@@ -349,7 +367,7 @@
  * @param n the number of elements to add
  * @return the number of added elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListAddArray(
         CxList *list,
         const void *array,
@@ -361,17 +379,17 @@
 /**
  * Inserts an item at the specified index.
  *
- * If \p index equals the list \c size, this is effectively cxListAdd().
+ * If @p index equals the list @c size, this is effectively cxListAdd().
  *
  * @param list the list
  * @param index the index the element shall have
  * @param elem a pointer to the element to add
- * @return zero on success, non-zero on memory allocation failure
- * or when the index is out of bounds
+ * @retval zero success
+ * @retval non-zero memory allocation failure or the index is out of bounds
  * @see cxListInsertAfter()
  * @see cxListInsertBefore()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListInsert(
         CxList *list,
         size_t index,
@@ -385,9 +403,10 @@
  *
  * @param list the list
  * @param elem a pointer to the element to add
- * @return zero on success, non-zero on memory allocation failure
+ * @retval zero success
+ * @retval non-zero memory allocation failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListInsertSorted(
         CxList *list,
         const void *elem
@@ -398,15 +417,15 @@
 
 /**
  * Inserts multiple items to the list at the specified index.
- * If \p index equals the list size, this is effectively cxListAddArray().
+ * If @p index equals the list size, this is effectively cxListAddArray().
  *
  * This method is usually more efficient than invoking cxListInsert()
  * multiple times.
  *
  * If there is not enough memory to add all elements, the returned value is
- * less than \p n.
+ * less than @p n.
  *
- * If this list is storing pointers instead of objects \p array is expected to
+ * If this list is storing pointers instead of objects @p array is expected to
  * be an array of pointers.
  *
  * @param list the list
@@ -415,7 +434,7 @@
  * @param n the number of elements to add
  * @return the number of added elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListInsertArray(
         CxList *list,
         size_t index,
@@ -432,9 +451,9 @@
  * because consecutive chunks of sorted data are inserted in one pass.
  *
  * If there is not enough memory to add all elements, the returned value is
- * less than \p n.
+ * less than @p n.
  *
- * If this list is storing pointers instead of objects \p array is expected to
+ * If this list is storing pointers instead of objects @p array is expected to
  * be an array of pointers.
  *
  * @param list the list
@@ -442,7 +461,7 @@
  * @param n the number of elements to add
  * @return the number of added elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListInsertSortedArray(
         CxList *list,
         const void *array,
@@ -457,16 +476,17 @@
  * The used iterator remains operational, but all other active iterators should
  * be considered invalidated.
  *
- * If \p iter is not a list iterator, the behavior is undefined.
- * If \p iter is a past-the-end iterator, the new element gets appended to the list.
+ * If @p iter is not a list iterator, the behavior is undefined.
+ * If @p iter is a past-the-end iterator, the new element gets appended to the list.
  *
  * @param iter an iterator
  * @param elem the element to insert
- * @return zero on success, non-zero on memory allocation failure
+ * @retval zero success
+ * @retval non-zero memory allocation failure
  * @see cxListInsert()
  * @see cxListInsertBefore()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListInsertAfter(
         CxIterator *iter,
         const void *elem
@@ -480,16 +500,17 @@
  * The used iterator remains operational, but all other active iterators should
  * be considered invalidated.
  *
- * If \p iter is not a list iterator, the behavior is undefined.
- * If \p iter is a past-the-end iterator, the new element gets appended to the list.
+ * If @p iter is not a list iterator, the behavior is undefined.
+ * If @p iter is a past-the-end iterator, the new element gets appended to the list.
  *
  * @param iter an iterator
  * @param elem the element to insert
- * @return zero on success, non-zero on memory allocation failure
+ * @retval zero success
+ * @retval non-zero memory allocation failure
  * @see cxListInsert()
  * @see cxListInsertAfter()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListInsertBefore(
         CxIterator *iter,
         const void *elem
@@ -505,25 +526,95 @@
  *
  * @param list the list
  * @param index the index of the element
- * @return zero on success, non-zero if the index is out of bounds
+ * @retval zero success
+ * @retval non-zero index out of bounds
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListRemove(
         CxList *list,
         size_t index
 ) {
-    return list->cl->remove(list, index);
+    return list->cl->remove(list, index, 1, NULL) == 0;
+}
+
+/**
+ * Removes and returns the element at the specified index.
+ *
+ * No destructor is called and instead the element is copied to the
+ * @p targetbuf which MUST be large enough to hold the removed element.
+ *
+ * @param list the list
+ * @param index the index of the element
+ * @param targetbuf a buffer where to copy the element
+ * @retval zero success
+ * @retval non-zero index out of bounds
+ */
+cx_attr_nonnull
+cx_attr_access_w(3)
+static inline int cxListRemoveAndGet(
+        CxList *list,
+        size_t index,
+        void *targetbuf
+) {
+    return list->cl->remove(list, index, 1, targetbuf) == 0;
+}
+
+/**
+ * Removes multiple element starting at the specified index.
+ *
+ * If an element destructor function is specified, it is called for each
+ * element. It is guaranteed that the destructor is called before removing
+ * the element, however, due to possible optimizations it is neither guaranteed
+ * that the destructors are invoked for all elements before starting to remove
+ * them, nor that the element is removed immediately after the destructor call
+ * before proceeding to the next element.
+ *
+ * @param list the list
+ * @param index the index of the element
+ * @param num the number of elements to remove
+ * @return the actual number of removed elements
+ */
+cx_attr_nonnull
+static inline size_t cxListRemoveArray(
+        CxList *list,
+        size_t index,
+        size_t num
+) {
+    return list->cl->remove(list, index, num, NULL);
+}
+
+/**
+ * Removes and returns multiple element starting at the specified index.
+ *
+ * No destructor is called and instead the elements are copied to the
+ * @p targetbuf which MUST be large enough to hold all removed elements.
+ *
+ * @param list the list
+ * @param index the index of the element
+ * @param num the number of elements to remove
+ * @param targetbuf a buffer where to copy the elements
+ * @return the actual number of removed elements
+ */
+cx_attr_nonnull
+cx_attr_access_w(4)
+static inline size_t cxListRemoveArrayAndGet(
+        CxList *list,
+        size_t index,
+        size_t num,
+        void *targetbuf
+) {
+    return list->cl->remove(list, index, num, targetbuf);
 }
 
 /**
  * Removes all elements from this list.
  *
- * If an element destructor function is specified, it is called for each
+ * If element destructor functions are specified, they are called for each
  * element before removing them.
  *
  * @param list the list
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxListClear(CxList *list) {
     list->cl->clear(list);
 }
@@ -537,9 +628,11 @@
  * @param list the list
  * @param i the index of the first element
  * @param j the index of the second element
- * @return zero on success, non-zero if one of the indices is out of bounds
+ * @retval zero success
+ * @retval non-zero one of the indices is out of bounds
+ * or the swap needed extra memory but allocation failed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListSwap(
         CxList *list,
         size_t i,
@@ -553,11 +646,11 @@
  *
  * @param list the list
  * @param index the index of the element
- * @return a pointer to the element or \c NULL if the index is out of bounds
+ * @return a pointer to the element or @c NULL if the index is out of bounds
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void *cxListAt(
-        CxList *list,
+        const CxList *list,
         size_t index
 ) {
     return list->cl->at(list, index);
@@ -574,7 +667,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListIteratorAt(
         const CxList *list,
         size_t index
@@ -593,7 +687,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListBackwardsIteratorAt(
         const CxList *list,
         size_t index
@@ -612,7 +707,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxListMutIteratorAt(
         CxList *list,
         size_t index
@@ -630,7 +726,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxListMutBackwardsIteratorAt(
         CxList *list,
         size_t index
@@ -646,7 +743,8 @@
  * @param list the list
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListIterator(const CxList *list) {
     return list->cl->iterator(list, 0, false);
 }
@@ -661,7 +759,8 @@
  * @param list the list
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListMutIterator(CxList *list) {
     return cxListMutIteratorAt(list, 0);
 }
@@ -677,7 +776,8 @@
  * @param list the list
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListBackwardsIterator(const CxList *list) {
     return list->cl->iterator(list, list->collection.size - 1, true);
 }
@@ -692,13 +792,14 @@
  * @param list the list
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListMutBackwardsIterator(CxList *list) {
     return cxListMutBackwardsIteratorAt(list, list->collection.size - 1);
 }
 
 /**
- * Returns the index of the first element that equals \p elem.
+ * Returns the index of the first element that equals @p elem.
  *
  * Determining equality is performed by the list's comparator function.
  *
@@ -707,7 +808,8 @@
  * @return the index of the element or a negative
  * value when the element is not found
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline ssize_t cxListFind(
         const CxList *list,
         const void *elem
@@ -716,7 +818,7 @@
 }
 
 /**
- * Removes and returns the index of the first element that equals \p elem.
+ * Removes and returns the index of the first element that equals @p elem.
  *
  * Determining equality is performed by the list's comparator function.
  *
@@ -725,7 +827,7 @@
  * @return the index of the now removed element or a negative
  * value when the element is not found or could not be removed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline ssize_t cxListFindRemove(
         CxList *list,
         const void *elem
@@ -734,13 +836,13 @@
 }
 
 /**
- * Sorts the list in-place.
+ * Sorts the list.
  *
- * \remark The underlying sort algorithm is implementation defined.
+ * @remark The underlying sort algorithm is implementation defined.
  *
  * @param list the list
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxListSort(CxList *list) {
     list->cl->sort(list);
 }
@@ -750,7 +852,7 @@
  *
  * @param list the list
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxListReverse(CxList *list) {
     list->cl->reverse(list);
 }
@@ -763,10 +865,14 @@
  *
  * @param list the list
  * @param other the list to compare to
- * @return zero, if both lists are equal element wise,
- * negative if the first list is smaller, positive if the first list is larger
+ * @retval zero both lists are equal element wise
+ * @retval negative the first list is smaller
+ * or the first non-equal element in the first list is smaller
+ * @retval positive the first list is larger
+ * or the first non-equal element in the first list is larger
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 int cxListCompare(
         const CxList *list,
         const CxList *other
@@ -775,22 +881,21 @@
 /**
  * Deallocates the memory of the specified list structure.
  *
- * Also calls content a destructor function, depending on the configuration
- * in CxList.content_destructor_type.
- *
- * This function itself is a destructor function for the CxList.
+ * Also calls the content destructor functions for each element, if specified.
  *
- * @param list the list which shall be destroyed
+ * @param list the list which shall be freed
  */
-__attribute__((__nonnull__))
-void cxListDestroy(CxList *list);
+void cxListFree(CxList *list);
 
 /**
  * A shared instance of an empty list.
  *
- * Writing to that list is undefined.
+ * Writing to that list is not allowed.
+ *
+ * You can use this is a placeholder for initializing CxList pointers
+ * for which you do not want to reserve memory right from the beginning.
  */
-extern CxList * const cxEmptyList;
+extern CxList *const cxEmptyList;
 
 
 #ifdef __cplusplus
--- a/ucx/cx/map.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/map.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file map.h
- * \brief Interface for map implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file map.h
+ * @brief Interface for map implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_MAP_H
@@ -89,19 +89,16 @@
     /**
      * Deallocates the entire memory.
      */
-    __attribute__((__nonnull__))
-    void (*destructor)(struct cx_map_s *map);
+    void (*deallocate)(struct cx_map_s *map);
 
     /**
      * Removes all elements.
      */
-    __attribute__((__nonnull__))
     void (*clear)(struct cx_map_s *map);
 
     /**
      * Add or overwrite an element.
      */
-    __attribute__((__nonnull__))
     int (*put)(
             CxMap *map,
             CxHashKey key,
@@ -111,7 +108,6 @@
     /**
      * Returns an element.
      */
-    __attribute__((__nonnull__, __warn_unused_result__))
     void *(*get)(
             const CxMap *map,
             CxHashKey key
@@ -119,18 +115,23 @@
 
     /**
      * Removes an element.
+     *
+     * Implementations SHALL check if @p targetbuf is set and copy the elements
+     * to the buffer without invoking any destructor.
+     * When @p targetbuf is not set, the destructors SHALL be invoked.
+     *
+     * The function SHALL return zero when the @p key was found and
+     * non-zero, otherwise. 
      */
-    __attribute__((__nonnull__))
-    void *(*remove)(
+    int (*remove)(
             CxMap *map,
             CxHashKey key,
-            bool destroy
+            void *targetbuf
     );
 
     /**
      * Creates an iterator for this map.
      */
-    __attribute__((__nonnull__, __warn_unused_result__))
     CxIterator (*iterator)(const CxMap *map, enum cx_map_iterator_type type);
 };
 
@@ -151,7 +152,10 @@
 /**
  * A shared instance of an empty map.
  *
- * Writing to that map is undefined.
+ * Writing to that map is not allowed.
+ *
+ * You can use this is a placeholder for initializing CxMap pointers
+ * for which you do not want to reserve memory right from the beginning.
  */
 extern CxMap *const cxEmptyMap;
 
@@ -164,7 +168,7 @@
  * @param map the map
  * @see cxMapStorePointers()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapStoreObjects(CxMap *map) {
     map->collection.store_pointer = false;
 }
@@ -181,7 +185,7 @@
  * @param map the map
  * @see cxMapStoreObjects()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapStorePointers(CxMap *map) {
     map->collection.store_pointer = true;
     map->collection.elem_size = sizeof(void *);
@@ -194,7 +198,7 @@
  * @return true, if this map is storing pointers
  * @see cxMapStorePointers()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxMapIsStoringPointers(const CxMap *map) {
     return map->collection.store_pointer;
 }
@@ -202,20 +206,21 @@
 /**
  * Deallocates the memory of the specified map.
  *
- * @param map the map to be destroyed
+ * Also calls the content destructor functions for each element, if specified.
+ *
+ * @param map the map to be freed
  */
-__attribute__((__nonnull__))
-static inline void cxMapDestroy(CxMap *map) {
-    map->cl->destructor(map);
-}
+void cxMapFree(CxMap *map);
 
 
 /**
  * Clears a map by removing all elements.
  *
+ * Also calls the content destructor functions for each element, if specified.
+ *
  * @param map the map to be cleared
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapClear(CxMap *map) {
     map->cl->clear(map);
 }
@@ -226,24 +231,22 @@
  * @param map the map
  * @return the number of stored elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxMapSize(const CxMap *map) {
     return map->collection.size;
 }
 
-
-// TODO: set-like map operations (union, intersect, difference)
-
 /**
  * Creates a value iterator for a map.
  *
- * \note An iterator iterates over all elements successively. Therefore the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
  * @return an iterator for the currently stored values
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxMapIteratorValues(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_VALUES);
 }
@@ -253,13 +256,14 @@
  *
  * The elements of the iterator are keys of type CxHashKey.
  *
- * \note An iterator iterates over all elements successively. Therefore the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
  * @return an iterator for the currently stored keys
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxMapIteratorKeys(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_KEYS);
 }
@@ -269,7 +273,7 @@
  *
  * The elements of the iterator are key/value pairs of type CxMapEntry.
  *
- * \note An iterator iterates over all elements successively. Therefore the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
@@ -277,7 +281,8 @@
  * @see cxMapIteratorKeys()
  * @see cxMapIteratorValues()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxMapIterator(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_PAIRS);
 }
@@ -286,13 +291,14 @@
 /**
  * Creates a mutating iterator over the values of a map.
  *
- * \note An iterator iterates over all elements successively. Therefore the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
  * @return an iterator for the currently stored values
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxMapMutIteratorValues(CxMap *map);
 
 /**
@@ -300,13 +306,14 @@
  *
  * The elements of the iterator are keys of type CxHashKey.
  *
- * \note An iterator iterates over all elements successively. Therefore the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
  * @return an iterator for the currently stored keys
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxMapMutIteratorKeys(CxMap *map);
 
 /**
@@ -314,7 +321,7 @@
  *
  * The elements of the iterator are key/value pairs of type CxMapEntry.
  *
- * \note An iterator iterates over all elements successively. Therefore the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
@@ -322,21 +329,13 @@
  * @see cxMapMutIteratorKeys()
  * @see cxMapMutIteratorValues()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxMapMutIterator(CxMap *map);
 
 #ifdef __cplusplus
 } // end the extern "C" block here, because we want to start overloading
-
-/**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
- */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
         CxHashKey const &key,
@@ -345,16 +344,7 @@
     return map->cl->put(map, key, value);
 }
 
-
-/**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
- */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
         cxstring const &key,
@@ -363,15 +353,7 @@
     return map->cl->put(map, cx_hash_key_cxstr(key), value);
 }
 
-/**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
- */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
         cxmutstr const &key,
@@ -380,15 +362,8 @@
     return map->cl->put(map, cx_hash_key_cxstr(key), value);
 }
 
-/**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
- */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline int cxMapPut(
         CxMap *map,
         const char *key,
@@ -397,14 +372,8 @@
     return map->cl->put(map, cx_hash_key_str(key), value);
 }
 
-/**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
- */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapGet(
         const CxMap *map,
         CxHashKey const &key
@@ -412,14 +381,8 @@
     return map->cl->get(map, key);
 }
 
-/**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
- */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapGet(
         const CxMap *map,
         cxstring const &key
@@ -427,14 +390,8 @@
     return map->cl->get(map, cx_hash_key_cxstr(key));
 }
 
-/**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
- */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapGet(
         const CxMap *map,
         cxmutstr const &key
@@ -442,14 +399,9 @@
     return map->cl->get(map, cx_hash_key_cxstr(key));
 }
 
-/**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
- */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(2)
 static inline void *cxMapGet(
         const CxMap *map,
         const char *key
@@ -457,301 +409,86 @@
     return map->cl->get(map, cx_hash_key_str(key));
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructor function, if any, on the removed element.
- * If this map is storing pointers and you just want to retrieve the pointer
- * without invoking the destructor, use cxMapRemoveAndGet().
- * If you just want to detach the element from the map without invoking the
- * destructor or returning the element, use cxMapDetach().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemoveAndGet()
- * @see cxMapDetach()
- */
-__attribute__((__nonnull__))
-static inline void cxMapRemove(
+cx_attr_nonnull
+static inline int cxMapRemove(
         CxMap *map,
         CxHashKey const &key
 ) {
-    (void) map->cl->remove(map, key, true);
-}
-
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructor function, if any, on the removed element.
- * If this map is storing pointers and you just want to retrieve the pointer
- * without invoking the destructor, use cxMapRemoveAndGet().
- * If you just want to detach the element from the map without invoking the
- * destructor or returning the element, use cxMapDetach().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemoveAndGet()
- * @see cxMapDetach()
- */
-__attribute__((__nonnull__))
-static inline void cxMapRemove(
-        CxMap *map,
-        cxstring const &key
-) {
-    (void) map->cl->remove(map, cx_hash_key_cxstr(key), true);
-}
-
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructor function, if any, on the removed element.
- * If this map is storing pointers and you just want to retrieve the pointer
- * without invoking the destructor, use cxMapRemoveAndGet().
- * If you just want to detach the element from the map without invoking the
- * destructor or returning the element, use cxMapDetach().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemoveAndGet()
- * @see cxMapDetach()
- */
-__attribute__((__nonnull__))
-static inline void cxMapRemove(
-        CxMap *map,
-        cxmutstr const &key
-) {
-    (void) map->cl->remove(map, cx_hash_key_cxstr(key), true);
+    return map->cl->remove(map, key, nullptr);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructor function, if any, on the removed element.
- * If this map is storing pointers and you just want to retrieve the pointer
- * without invoking the destructor, use cxMapRemoveAndGet().
- * If you just want to detach the element from the map without invoking the
- * destructor or returning the element, use cxMapDetach().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemoveAndGet()
- * @see cxMapDetach()
- */
-__attribute__((__nonnull__))
-static inline void cxMapRemove(
-        CxMap *map,
-        const char *key
-) {
-    (void) map->cl->remove(map, cx_hash_key_str(key), true);
-}
-
-/**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * In general, you should only use this function if the map does not own
- * the data and there is a valid reference to the data somewhere else
- * in the program. In all other cases it is preferable to use
- * cxMapRemove() or cxMapRemoveAndGet().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemove()
- * @see cxMapRemoveAndGet()
- */
-__attribute__((__nonnull__))
-static inline void cxMapDetach(
-        CxMap *map,
-        CxHashKey const &key
-) {
-    (void) map->cl->remove(map, key, false);
-}
-
-/**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * In general, you should only use this function if the map does not own
- * the data and there is a valid reference to the data somewhere else
- * in the program. In all other cases it is preferable to use
- * cxMapRemove() or cxMapRemoveAndGet().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemove()
- * @see cxMapRemoveAndGet()
- */
-__attribute__((__nonnull__))
-static inline void cxMapDetach(
+cx_attr_nonnull
+static inline int cxMapRemove(
         CxMap *map,
         cxstring const &key
 ) {
-    (void) map->cl->remove(map, cx_hash_key_cxstr(key), false);
+    return map->cl->remove(map, cx_hash_key_cxstr(key), nullptr);
 }
 
-/**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * In general, you should only use this function if the map does not own
- * the data and there is a valid reference to the data somewhere else
- * in the program. In all other cases it is preferable to use
- * cxMapRemove() or cxMapRemoveAndGet().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemove()
- * @see cxMapRemoveAndGet()
- */
-__attribute__((__nonnull__))
-static inline void cxMapDetach(
+cx_attr_nonnull
+static inline int cxMapRemove(
         CxMap *map,
         cxmutstr const &key
 ) {
-    (void) map->cl->remove(map, cx_hash_key_cxstr(key), false);
+    return map->cl->remove(map, cx_hash_key_cxstr(key), nullptr);
 }
 
-/**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * In general, you should only use this function if the map does not own
- * the data and there is a valid reference to the data somewhere else
- * in the program. In all other cases it is preferable to use
- * cxMapRemove() or cxMapRemoveAndGet().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemove()
- * @see cxMapRemoveAndGet()
- */
-__attribute__((__nonnull__))
-static inline void cxMapDetach(
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
+static inline int cxMapRemove(
         CxMap *map,
         const char *key
 ) {
-    (void) map->cl->remove(map, cx_hash_key_str(key), false);
+    return map->cl->remove(map, cx_hash_key_str(key), nullptr);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function can be used when the map is storing pointers,
- * in order to retrieve the pointer from the map without invoking
- * any destructor function. Sometimes you do not want the pointer
- * to be returned - in that case (instead of suppressing the "unused
- * result" warning) you can use cxMapDetach().
- *
- * If this map is not storing pointers, this function behaves like
- * cxMapRemove() and returns \c NULL.
- *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
- * @see cxMapStorePointers()
- * @see cxMapDetach()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline void *cxMapRemoveAndGet(
+cx_attr_nonnull
+cx_attr_access_w(3)
+static inline int cxMapRemoveAndGet(
         CxMap *map,
-        CxHashKey key
+        CxHashKey key,
+        void *targetbuf
 ) {
-    return map->cl->remove(map, key, !map->collection.store_pointer);
+    return map->cl->remove(map, key, targetbuf);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function can be used when the map is storing pointers,
- * in order to retrieve the pointer from the map without invoking
- * any destructor function. Sometimes you do not want the pointer
- * to be returned - in that case (instead of suppressing the "unused
- * result" warning) you can use cxMapDetach().
- *
- * If this map is not storing pointers, this function behaves like
- * cxMapRemove() and returns \c NULL.
- *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
- * @see cxMapStorePointers()
- * @see cxMapDetach()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline void *cxMapRemoveAndGet(
+cx_attr_nonnull
+cx_attr_access_w(3)
+static inline int cxMapRemoveAndGet(
         CxMap *map,
-        cxstring key
+        cxstring key,
+        void *targetbuf
 ) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), !map->collection.store_pointer);
+    return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function can be used when the map is storing pointers,
- * in order to retrieve the pointer from the map without invoking
- * any destructor function. Sometimes you do not want the pointer
- * to be returned - in that case (instead of suppressing the "unused
- * result" warning) you can use cxMapDetach().
- *
- * If this map is not storing pointers, this function behaves like
- * cxMapRemove() and returns \c NULL.
- *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
- * @see cxMapStorePointers()
- * @see cxMapDetach()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline void *cxMapRemoveAndGet(
+cx_attr_nonnull
+cx_attr_access_w(3)
+static inline int cxMapRemoveAndGet(
         CxMap *map,
-        cxmutstr key
+        cxmutstr key,
+        void *targetbuf
 ) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), !map->collection.store_pointer);
+    return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function can be used when the map is storing pointers,
- * in order to retrieve the pointer from the map without invoking
- * any destructor function. Sometimes you do not want the pointer
- * to be returned - in that case (instead of suppressing the "unused
- * result" warning) you can use cxMapDetach().
- *
- * If this map is not storing pointers, this function behaves like
- * cxMapRemove() and returns \c NULL.
- *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
- * @see cxMapStorePointers()
- * @see cxMapDetach()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline void *cxMapRemoveAndGet(
+cx_attr_nonnull
+cx_attr_access_w(3)
+cx_attr_cstr_arg(2)
+static inline int cxMapRemoveAndGet(
         CxMap *map,
-        const char *key
+        const char *key,
+        void *targetbuf
 ) {
-    return map->cl->remove(map, cx_hash_key_str(key), !map->collection.store_pointer);
+    return map->cl->remove(map, cx_hash_key_str(key), targetbuf);
 }
 
 #else // __cplusplus
 
 /**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * @copydoc cxMapPut()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_map_put(
         CxMap *map,
         CxHashKey key,
@@ -761,14 +498,9 @@
 }
 
 /**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * @copydoc cxMapPut()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_map_put_cxstr(
         CxMap *map,
         cxstring key,
@@ -778,14 +510,9 @@
 }
 
 /**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * @copydoc cxMapPut()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_map_put_mustr(
         CxMap *map,
         cxmutstr key,
@@ -795,14 +522,10 @@
 }
 
 /**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * @copydoc cxMapPut()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline int cx_map_put_str(
         CxMap *map,
         const char *key,
@@ -814,10 +537,19 @@
 /**
  * Puts a key/value-pair into the map.
  *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * A possible existing value will be overwritten.
+ *
+ * If this map is storing pointers, the @p value pointer is written
+ * to the map. Otherwise, the memory is copied from @p value with
+ * memcpy().
+ *
+ * The @p key is always copied.
+ *
+ * @param map (@c CxMap*) the map
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param value (@c void*) the value
+ * @retval zero success
+ * @retval non-zero value on memory allocation failure
  */
 #define cxMapPut(map, key, value) _Generic((key), \
     CxHashKey: cx_map_put,                        \
@@ -828,13 +560,10 @@
     (map, key, value)
 
 /**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
+ * @copydoc cxMapGet()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_get(
         const CxMap *map,
         CxHashKey key
@@ -843,13 +572,10 @@
 }
 
 /**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
+ * @copydoc cxMapGet()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_get_cxstr(
         const CxMap *map,
         cxstring key
@@ -858,13 +584,10 @@
 }
 
 /**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
+ * @copydoc cxMapGet()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_get_mustr(
         const CxMap *map,
         cxmutstr key
@@ -873,13 +596,11 @@
 }
 
 /**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
+ * @copydoc cxMapGet()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(2)
 static inline void *cx_map_get_str(
         const CxMap *map,
         const char *key
@@ -890,9 +611,13 @@
 /**
  * Retrieves a value by using a key.
  *
- * @param map the map
- * @param key the key
- * @return the value
+ * If this map is storing pointers, the stored pointer is returned.
+ * Otherwise, a pointer to the element within the map's memory
+ * is returned (which is valid as long as the element stays in the map).
+ *
+ * @param map (@c CxMap*) the map
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @return (@c void*) the value
  */
 #define cxMapGet(map, key) _Generic((key), \
     CxHashKey: cx_map_get,                 \
@@ -903,74 +628,61 @@
     (map, key)
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * @param map the map
- * @param key the key
+ * @copydoc cxMapRemove()
  */
-__attribute__((__nonnull__))
-static inline void cx_map_remove(
+cx_attr_nonnull
+static inline int cx_map_remove(
         CxMap *map,
         CxHashKey key
 ) {
-    (void) map->cl->remove(map, key, true);
+    return map->cl->remove(map, key, NULL);
+}
+
+/**
+ * @copydoc cxMapRemove()
+ */
+cx_attr_nonnull
+static inline int cx_map_remove_cxstr(
+        CxMap *map,
+        cxstring key
+) {
+    return map->cl->remove(map, cx_hash_key_cxstr(key), NULL);
 }
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * @param map the map
- * @param key the key
+ * @copydoc cxMapRemove()
  */
-__attribute__((__nonnull__))
-static inline void cx_map_remove_cxstr(
+cx_attr_nonnull
+static inline int cx_map_remove_mustr(
         CxMap *map,
-        cxstring key
+        cxmutstr key
 ) {
-    (void) map->cl->remove(map, cx_hash_key_cxstr(key), true);
+    return map->cl->remove(map, cx_hash_key_cxstr(key), NULL);
+}
+
+/**
+ * @copydoc cxMapRemove()
+ */
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
+static inline int cx_map_remove_str(
+        CxMap *map,
+        const char *key
+) {
+    return map->cl->remove(map, cx_hash_key_str(key), NULL);
 }
 
 /**
  * Removes a key/value-pair from the map by using the key.
  *
- * @param map the map
- * @param key the key
- */
-__attribute__((__nonnull__))
-static inline void cx_map_remove_mustr(
-        CxMap *map,
-        cxmutstr key
-) {
-    (void) map->cl->remove(map, cx_hash_key_cxstr(key), true);
-}
-
-/**
- * Removes a key/value-pair from the map by using the key.
+ * Always invokes the destructors functions, if any, on the removed element.
  *
- * @param map the map
- * @param key the key
- */
-__attribute__((__nonnull__))
-static inline void cx_map_remove_str(
-        CxMap *map,
-        const char *key
-) {
-    (void) map->cl->remove(map, cx_hash_key_str(key), true);
-}
-
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructor function, if any, on the removed element.
- * If this map is storing pointers and you just want to retrieve the pointer
- * without invoking the destructor, use cxMapRemoveAndGet().
- * If you just want to detach the element from the map without invoking the
- * destructor or returning the element, use cxMapDetach().
- *
- * @param map the map
- * @param key the key
+ * @param map (@c CxMap*) the map
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @retval zero success
+ * @retval non-zero the key was not found
+ * 
  * @see cxMapRemoveAndGet()
- * @see cxMapDetach()
  */
 #define cxMapRemove(map, key) _Generic((key), \
     CxHashKey: cx_map_remove,                 \
@@ -981,177 +693,85 @@
     (map, key)
 
 /**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * @param map the map
- * @param key the key
- */
-__attribute__((__nonnull__))
-static inline void cx_map_detach(
-        CxMap *map,
-        CxHashKey key
-) {
-    (void) map->cl->remove(map, key, false);
-}
-
-/**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * @param map the map
- * @param key the key
+ * @copydoc cxMapRemoveAndGet()
  */
-__attribute__((__nonnull__))
-static inline void cx_map_detach_cxstr(
+cx_attr_nonnull
+cx_attr_access_w(3)
+static inline int cx_map_remove_and_get(
         CxMap *map,
-        cxstring key
+        CxHashKey key,
+        void *targetbuf
 ) {
-    (void) map->cl->remove(map, cx_hash_key_cxstr(key), false);
-}
-
-/**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * @param map the map
- * @param key the key
- */
-__attribute__((__nonnull__))
-static inline void cx_map_detach_mustr(
-        CxMap *map,
-        cxmutstr key
-) {
-    (void) map->cl->remove(map, cx_hash_key_cxstr(key), false);
+    return map->cl->remove(map, key, targetbuf);
 }
 
 /**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * @param map the map
- * @param key the key
+ * @copydoc cxMapRemoveAndGet()
  */
-__attribute__((__nonnull__))
-static inline void cx_map_detach_str(
+cx_attr_nonnull
+cx_attr_access_w(3)
+static inline int cx_map_remove_and_get_cxstr(
         CxMap *map,
-        const char *key
+        cxstring key,
+        void *targetbuf
 ) {
-    (void) map->cl->remove(map, cx_hash_key_str(key), false);
+    return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
 }
 
 /**
- * Detaches a key/value-pair from the map by using the key
- * without invoking the destructor.
- *
- * In general, you should only use this function if the map does not own
- * the data and there is a valid reference to the data somewhere else
- * in the program. In all other cases it is preferable to use
- * cxMapRemove() or cxMapRemoveAndGet().
- *
- * @param map the map
- * @param key the key
- * @see cxMapRemove()
- * @see cxMapRemoveAndGet()
+ * @copydoc cxMapRemoveAndGet()
  */
-#define cxMapDetach(map, key) _Generic((key), \
-    CxHashKey: cx_map_detach,                 \
-    cxstring: cx_map_detach_cxstr,            \
-    cxmutstr: cx_map_detach_mustr,            \
-    char*: cx_map_detach_str,                 \
-    const char*: cx_map_detach_str)           \
-    (map, key)
+cx_attr_nonnull
+cx_attr_access_w(3)
+static inline int cx_map_remove_and_get_mustr(
+        CxMap *map,
+        cxmutstr key,
+        void *targetbuf
+) {
+    return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
+}
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
+ * @copydoc cxMapRemoveAndGet()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline void *cx_map_remove_and_get(
+cx_attr_nonnull
+cx_attr_access_w(3)
+cx_attr_cstr_arg(2)
+static inline int cx_map_remove_and_get_str(
         CxMap *map,
-        CxHashKey key
+        const char *key,
+        void *targetbuf
 ) {
-    return map->cl->remove(map, key, !map->collection.store_pointer);
+    return map->cl->remove(map, cx_hash_key_str(key), targetbuf);
 }
 
 /**
  * Removes a key/value-pair from the map by using the key.
  *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline void *cx_map_remove_and_get_cxstr(
-        CxMap *map,
-        cxstring key
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), !map->collection.store_pointer);
-}
-
-/**
- * Removes a key/value-pair from the map by using the key.
+ * This function will copy the contents of the removed element
+ * to the target buffer must be guaranteed to be large enough
+ * to hold the element (the map's element size).
+ * The destructor functions, if any, will @em not be called.
  *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline void *cx_map_remove_and_get_mustr(
-        CxMap *map,
-        cxmutstr key
-) {
-    return map->cl->remove(map, cx_hash_key_cxstr(key), !map->collection.store_pointer);
-}
-
-/**
- * Removes a key/value-pair from the map by using the key.
+ * If this map is storing pointers, the element is the pointer itself
+ * and not the object it points to.
  *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
+ * @param map (@c CxMap*) the map
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param targetbuf (@c void*) the buffer where the element shall be copied to
+ * @retval zero success
+ * @retval non-zero the key was not found
+ * 
+ * @see cxMapStorePointers()
+ * @see cxMapRemove()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline void *cx_map_remove_and_get_str(
-        CxMap *map,
-        const char *key
-) {
-    return map->cl->remove(map, cx_hash_key_str(key), !map->collection.store_pointer);
-}
-
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function can be used when the map is storing pointers,
- * in order to retrieve the pointer from the map without invoking
- * any destructor function. Sometimes you do not want the pointer
- * to be returned - in that case (instead of suppressing the "unused
- * result" warning) you can use cxMapDetach().
- *
- * If this map is not storing pointers, this function behaves like
- * cxMapRemove() and returns \c NULL.
- *
- * @param map the map
- * @param key the key
- * @return the stored pointer or \c NULL if either the key is not present
- * in the map or the map is not storing pointers
- * @see cxMapStorePointers()
- * @see cxMapDetach()
- */
-#define cxMapRemoveAndGet(map, key) _Generic((key), \
+#define cxMapRemoveAndGet(map, key, targetbuf) _Generic((key), \
     CxHashKey: cx_map_remove_and_get,               \
     cxstring: cx_map_remove_and_get_cxstr,          \
     cxmutstr: cx_map_remove_and_get_mustr,          \
     char*: cx_map_remove_and_get_str,               \
     const char*: cx_map_remove_and_get_str)         \
-    (map, key)
+    (map, key, targetbuf)
 
 #endif // __cplusplus
 
--- a/ucx/cx/mempool.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/mempool.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file mempool.h
- * \brief Interface for memory pool implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file mempool.h
+ * @brief Interface for memory pool implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_MEMPOOL_H
@@ -76,35 +76,33 @@
 typedef struct cx_mempool_s CxMempool;
 
 /**
+ * Deallocates a memory pool and frees the managed memory.
+ *
+ * @param pool the memory pool to free
+ */
+void cxMempoolFree(CxMempool *pool);
+
+/**
  * Creates an array-based memory pool with a shared destructor function.
  *
  * This destructor MUST NOT free the memory.
  *
  * @param capacity the initial capacity of the pool
- * @param destr the destructor function to use for allocated memory
- * @return the created memory pool or \c NULL if allocation failed
+ * @param destr optional destructor function to use for allocated memory
+ * @return the created memory pool or @c NULL if allocation failed
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxMempoolFree, 1)
 CxMempool *cxMempoolCreate(size_t capacity, cx_destructor_func destr);
 
 /**
  * Creates a basic array-based memory pool.
  *
- * @param capacity the initial capacity of the pool
- * @return the created memory pool or \c NULL if allocation failed
+ * @param capacity (@c size_t) the initial capacity of the pool
+ * @return (@c CxMempool*) the created memory pool or @c NULL if allocation failed
  */
-__attribute__((__warn_unused_result__))
-static inline CxMempool *cxBasicMempoolCreate(size_t capacity) {
-    return cxMempoolCreate(capacity, NULL);
-}
-
-/**
- * Destroys a memory pool and frees the managed memory.
- *
- * @param pool the memory pool to destroy
- */
-__attribute__((__nonnull__))
-void cxMempoolDestroy(CxMempool *pool);
+#define cxBasicMempoolCreate(capacity) cxMempoolCreate(capacity, NULL)
 
 /**
  * Sets the destructor function for a specific allocated memory object.
@@ -115,13 +113,24 @@
  * @param memory the object allocated in the pool
  * @param fnc the destructor function
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxMempoolSetDestructor(
         void *memory,
         cx_destructor_func fnc
 );
 
 /**
+ * Removes the destructor function for a specific allocated memory object.
+ *
+ * If the memory is not managed by a UCX memory pool, the behavior is undefined.
+ * The destructor MUST NOT free the memory.
+ *
+ * @param memory the object allocated in the pool
+ */
+cx_attr_nonnull
+void cxMempoolRemoveDestructor(void *memory);
+
+/**
  * Registers foreign memory with this pool.
  *
  * The destructor, in contrast to memory allocated by the pool, MUST free the memory.
@@ -130,11 +139,12 @@
  * If that allocation fails, this function will return non-zero.
  *
  * @param pool the pool
- * @param memory the object allocated in the pool
+ * @param memory the object to register (MUST NOT be already allocated in the pool)
  * @param destr the destructor function
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxMempoolRegister(
         CxMempool *pool,
         void *memory,
--- a/ucx/cx/printf.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/printf.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file printf.h
- * \brief Wrapper for write functions with a printf-like interface.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file printf.h
+ * @brief Wrapper for write functions with a printf-like interface.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_PRINTF_H
@@ -40,6 +40,14 @@
 #include "string.h"
 #include <stdarg.h>
 
+/**
+ * Attribute for printf-like functions.
+ * @param fmt_idx index of the format string parameter
+ * @param arg_idx index of the first formatting argument
+ */
+#define cx_attr_printf(fmt_idx, arg_idx) \
+    __attribute__((__format__(printf, fmt_idx, arg_idx)))
+
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -48,19 +56,21 @@
 /**
  * The maximum string length that fits into stack memory.
  */
-extern unsigned const cx_printf_sbo_size;
+extern const unsigned cx_printf_sbo_size;
 
 /**
- * A \c fprintf like function which writes the output to a stream by
+ * A @c fprintf like function which writes the output to a stream by
  * using a write_func.
  *
  * @param stream the stream the data is written to
  * @param wfc the write function
  * @param fmt format string
  * @param ... additional arguments
- * @return the total number of bytes written
+ * @return the total number of bytes written or an error code from stdlib printf implementation
  */
-__attribute__((__nonnull__(1, 2, 3), __format__(printf, 3, 4)))
+cx_attr_nonnull_arg(1, 2, 3)
+cx_attr_printf(3, 4)
+cx_attr_cstr_arg(3)
 int cx_fprintf(
         void *stream,
         cx_write_func wfc,
@@ -69,17 +79,18 @@
 );
 
 /**
- * A \c vfprintf like function which writes the output to a stream by
+ * A @c vfprintf like function which writes the output to a stream by
  * using a write_func.
  *
  * @param stream the stream the data is written to
  * @param wfc the write function
  * @param fmt format string
  * @param ap argument list
- * @return the total number of bytes written
+ * @return the total number of bytes written or an error code from stdlib printf implementation
  * @see cx_fprintf()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(3)
 int cx_vfprintf(
         void *stream,
         cx_write_func wfc,
@@ -88,10 +99,12 @@
 );
 
 /**
- * A \c asprintf like function which allocates space for a string
+ * A @c asprintf like function which allocates space for a string
  * the result is written to.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated,
+ * unless there was an error, in which case the string's pointer
+ * will be @c NULL.
  *
  * @param allocator the CxAllocator used for allocating the string
  * @param fmt format string
@@ -99,7 +112,9 @@
  * @return the formatted string
  * @see cx_strfree_a()
  */
-__attribute__((__nonnull__(1, 2), __format__(printf, 2, 3)))
+cx_attr_nonnull_arg(1, 2)
+cx_attr_printf(2, 3)
+cx_attr_cstr_arg(2)
 cxmutstr cx_asprintf_a(
         const CxAllocator *allocator,
         const char *fmt,
@@ -107,24 +122,28 @@
 );
 
 /**
- * A \c asprintf like function which allocates space for a string
+ * A @c asprintf like function which allocates space for a string
  * the result is written to.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated,
+ * unless there was an error, in which case the string's pointer
+ * will be @c NULL.
  *
- * @param fmt format string
+ * @param fmt (@c char*) format string
  * @param ... additional arguments
- * @return the formatted string
+ * @return (@c cxmutstr) the formatted string
  * @see cx_strfree()
  */
 #define cx_asprintf(fmt, ...) \
     cx_asprintf_a(cxDefaultAllocator, fmt, __VA_ARGS__)
 
 /**
-* A \c vasprintf like function which allocates space for a string
+* A @c vasprintf like function which allocates space for a string
  * the result is written to.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated,
+ * unless there was an error, in which case the string's pointer
+ * will be @c NULL.
  *
  * @param allocator the CxAllocator used for allocating the string
  * @param fmt format string
@@ -132,7 +151,8 @@
  * @return the formatted string
  * @see cx_asprintf_a()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 cxmutstr cx_vasprintf_a(
         const CxAllocator *allocator,
         const char *fmt,
@@ -140,192 +160,232 @@
 );
 
 /**
-* A \c vasprintf like function which allocates space for a string
+* A @c vasprintf like function which allocates space for a string
  * the result is written to.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated,
+ * unless there was in error, in which case the string's pointer
+ * will be @c NULL.
  *
- * @param fmt format string
- * @param ap argument list
- * @return the formatted string
+ * @param fmt (@c char*) format string
+ * @param ap (@c va_list) argument list
+ * @return (@c cxmutstr) the formatted string
  * @see cx_asprintf()
  */
 #define cx_vasprintf(fmt, ap) cx_vasprintf_a(cxDefaultAllocator, fmt, ap)
 
 /**
- * A \c printf like function which writes the output to a CxBuffer.
+ * A @c printf like function which writes the output to a CxBuffer.
  *
- * @param buffer a pointer to the buffer the data is written to
- * @param fmt the format string
+ * @param buffer (@c CxBuffer*) a pointer to the buffer the data is written to
+ * @param fmt (@c char*) the format string
  * @param ... additional arguments
- * @return the total number of bytes written
- * @see ucx_fprintf()
+ * @return (@c int) the total number of bytes written or an error code from stdlib printf implementation
+ * @see cx_fprintf()
+ * @see cxBufferWrite()
  */
-#define cx_bprintf(buffer, fmt, ...) cx_fprintf((CxBuffer*)buffer, \
+#define cx_bprintf(buffer, fmt, ...) cx_fprintf((void*)buffer, \
     (cx_write_func) cxBufferWrite, fmt, __VA_ARGS__)
 
 
 /**
- * An \c sprintf like function which reallocates the string when the buffer is not large enough.
+ * An @c sprintf like function which reallocates the string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  *
- * @param str a pointer to the string buffer
- * @param len a pointer to the length of the buffer
- * @param fmt the format string
+ * @param str (@c char**) a pointer to the string buffer
+ * @param len (@c size_t*) a pointer to the length of the buffer
+ * @param fmt (@c char*) the format string
  * @param ... additional arguments
- * @return the length of produced string
+ * @return (@c int) the length of produced string or an error code from stdlib printf implementation
  */
 #define cx_sprintf(str, len, fmt, ...) cx_sprintf_a(cxDefaultAllocator, str, len, fmt, __VA_ARGS__)
 
 /**
- * An \c sprintf like function which reallocates the string when the buffer is not large enough.
+ * An @c sprintf like function which reallocates the string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  *
- * \attention The original buffer MUST have been allocated with the same allocator!
+ * @attention The original buffer MUST have been allocated with the same allocator!
  *
  * @param alloc the allocator to use
  * @param str a pointer to the string buffer
  * @param len a pointer to the length of the buffer
  * @param fmt the format string
  * @param ... additional arguments
- * @return the length of produced string
+ * @return the length of produced string or an error code from stdlib printf implementation
  */
-__attribute__((__nonnull__(1, 2, 3, 4), __format__(printf, 4, 5)))
-int cx_sprintf_a(CxAllocator *alloc, char **str, size_t *len, const char *fmt, ... );
+cx_attr_nonnull_arg(1, 2, 3, 4)
+cx_attr_printf(4, 5)
+cx_attr_cstr_arg(4)
+int cx_sprintf_a(
+        CxAllocator *alloc,
+        char **str,
+        size_t *len,
+        const char *fmt,
+        ...
+);
 
 
 /**
- * An \c sprintf like function which reallocates the string when the buffer is not large enough.
+ * An @c sprintf like function which reallocates the string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  *
- * @param str a pointer to the string buffer
- * @param len a pointer to the length of the buffer
- * @param fmt the format string
- * @param ap argument list
- * @return the length of produced string
+ * @param str (@c char**) a pointer to the string buffer
+ * @param len (@c size_t*) a pointer to the length of the buffer
+ * @param fmt (@c char*) the format string
+ * @param ap (@c va_list) argument list
+ * @return (@c int) the length of produced string or an error code from stdlib printf implementation
  */
 #define cx_vsprintf(str, len, fmt, ap) cx_vsprintf_a(cxDefaultAllocator, str, len, fmt, ap)
 
 /**
- * An \c sprintf like function which reallocates the string when the buffer is not large enough.
+ * An @c sprintf like function which reallocates the string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated.
  *
- * \attention The original buffer MUST have been allocated with the same allocator!
+ * @attention The original buffer MUST have been allocated with the same allocator!
  *
  * @param alloc the allocator to use
  * @param str a pointer to the string buffer
  * @param len a pointer to the length of the buffer
  * @param fmt the format string
  * @param ap argument list
- * @return the length of produced string
+ * @return the length of produced string or an error code from stdlib printf implementation
  */
-__attribute__((__nonnull__))
-int cx_vsprintf_a(CxAllocator *alloc, char **str, size_t *len, const char *fmt, va_list ap);
+cx_attr_nonnull
+cx_attr_cstr_arg(4)
+cx_attr_access_rw(2)
+cx_attr_access_rw(3)
+int cx_vsprintf_a(
+        CxAllocator *alloc,
+        char **str,
+        size_t *len,
+        const char *fmt,
+        va_list ap
+);
 
 
 /**
- * An \c sprintf like function which allocates a new string when the buffer is not large enough.
+ * An @c sprintf like function which allocates a new string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * The location of the resulting string will \em always be stored to \p str. When the buffer
- * was sufficiently large, \p buf itself will be stored to the location of \p str.
+ * The location of the resulting string will @em always be stored to @p str. When the buffer
+ * was sufficiently large, @p buf itself will be stored to the location of @p str.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  * 
- * \remark When a new string needed to be allocated, the contents of \p buf will be
- * poisoned after the call, because this function tries to produce the string in \p buf, first.
+ * @remark When a new string needed to be allocated, the contents of @p buf will be
+ * poisoned after the call, because this function tries to produce the string in @p buf, first.
  *
- * @param buf a pointer to the buffer
- * @param len a pointer to the length of the buffer
- * @param str a pointer to the location
- * @param fmt the format string
+ * @param buf (@c char*) a pointer to the buffer
+ * @param len (@c size_t*) a pointer to the length of the buffer
+ * @param str (@c char**) a pointer where the location of the result shall be stored
+ * @param fmt (@c char*) the format string
  * @param ... additional arguments
- * @return the length of produced string
+ * @return (@c int) the length of produced string or an error code from stdlib printf implementation
  */
 #define cx_sprintf_s(buf, len, str, fmt, ...) cx_sprintf_sa(cxDefaultAllocator, buf, len, str, fmt, __VA_ARGS__)
 
 /**
- * An \c sprintf like function which allocates a new string when the buffer is not large enough.
+ * An @c sprintf like function which allocates a new string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * The location of the resulting string will \em always be stored to \p str. When the buffer
- * was sufficiently large, \p buf itself will be stored to the location of \p str.
+ * The location of the resulting string will @em always be stored to @p str. When the buffer
+ * was sufficiently large, @p buf itself will be stored to the location of @p str.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  *
- * \remark When a new string needed to be allocated, the contents of \p buf will be
- * poisoned after the call, because this function tries to produce the string in \p buf, first.
+ * @remark When a new string needed to be allocated, the contents of @p buf will be
+ * poisoned after the call, because this function tries to produce the string in @p buf, first.
  *
  * @param alloc the allocator to use
  * @param buf a pointer to the buffer
  * @param len a pointer to the length of the buffer
- * @param str a pointer to the location
+ * @param str a pointer where the location of the result shall be stored
  * @param fmt the format string
  * @param ... additional arguments
- * @return the length of produced string
+ * @return the length of produced string or an error code from stdlib printf implementation
  */
-__attribute__((__nonnull__(1, 2, 4, 5), __format__(printf, 5, 6)))
-int cx_sprintf_sa(CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, ... );
+cx_attr_nonnull_arg(1, 2, 4, 5)
+cx_attr_printf(5, 6)
+cx_attr_cstr_arg(5)
+cx_attr_access_rw(2)
+cx_attr_access_rw(3)
+cx_attr_access_rw(4)
+int cx_sprintf_sa(
+        CxAllocator *alloc,
+        char *buf,
+        size_t *len,
+        char **str,
+        const char *fmt,
+        ...
+);
 
 /**
- * An \c sprintf like function which allocates a new string when the buffer is not large enough.
+ * An @c sprintf like function which allocates a new string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * The location of the resulting string will \em always be stored to \p str. When the buffer
- * was sufficiently large, \p buf itself will be stored to the location of \p str.
+ * The location of the resulting string will @em always be stored to @p str. When the buffer
+ * was sufficiently large, @p buf itself will be stored to the location of @p str.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated.
  *
- * \remark When a new string needed to be allocated, the contents of \p buf will be
- * poisoned after the call, because this function tries to produce the string in \p buf, first.
+ * @remark When a new string needed to be allocated, the contents of @p buf will be
+ * poisoned after the call, because this function tries to produce the string in @p buf, first.
  *
- * @param buf a pointer to the buffer
- * @param len a pointer to the length of the buffer
- * @param str a pointer to the location
- * @param fmt the format string
- * @param ap argument list
- * @return the length of produced string
+ * @param buf (@c char*) a pointer to the buffer
+ * @param len (@c size_t*) a pointer to the length of the buffer
+ * @param str (@c char**) a pointer where the location of the result shall be stored
+ * @param fmt (@c char*) the format string
+ * @param ap (@c va_list) argument list
+ * @return (@c int) the length of produced string or an error code from stdlib printf implementation
  */
 #define cx_vsprintf_s(buf, len, str, fmt, ap) cx_vsprintf_sa(cxDefaultAllocator, buf, len, str, fmt, ap)
 
 /**
- * An \c sprintf like function which allocates a new string when the buffer is not large enough.
+ * An @c sprintf like function which allocates a new string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * The location of the resulting string will \em always be stored to \p str. When the buffer
- * was sufficiently large, \p buf itself will be stored to the location of \p str.
+ * The location of the resulting string will @em always be stored to @p str. When the buffer
+ * was sufficiently large, @p buf itself will be stored to the location of @p str.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated.
  *
- * \remark When a new string needed to be allocated, the contents of \p buf will be
- * poisoned after the call, because this function tries to produce the string in \p buf, first.
+ * @remark When a new string needed to be allocated, the contents of @p buf will be
+ * poisoned after the call, because this function tries to produce the string in @p buf, first.
  *
  * @param alloc the allocator to use
  * @param buf a pointer to the buffer
  * @param len a pointer to the length of the buffer
- * @param str a pointer to the location
+ * @param str a pointer where the location of the result shall be stored
  * @param fmt the format string
  * @param ap argument list
- * @return the length of produced string
+ * @return the length of produced string or an error code from stdlib printf implementation
  */
-__attribute__((__nonnull__))
-int cx_vsprintf_sa(CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, va_list ap);
+cx_attr_nonnull
+cx_attr_cstr_arg(5)
+int cx_vsprintf_sa(
+        CxAllocator *alloc,
+        char *buf,
+        size_t *len,
+        char **str,
+        const char *fmt,
+        va_list ap
+);
 
 
 #ifdef __cplusplus
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ucx/cx/properties.h	Sun Jan 05 22:00:39 2025 +0100
@@ -0,0 +1,644 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+/**
+ * @file properties.h
+ * @brief Interface for parsing data from properties files.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
+ */
+
+#ifndef UCX_PROPERTIES_H
+#define UCX_PROPERTIES_H
+
+#include "common.h"
+#include "string.h"
+#include "map.h"
+#include "buffer.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Configures the expected characters for the properties parser.
+ */
+struct cx_properties_config_s {
+    /**
+     * The key/value delimiter that shall be used.
+     * This is '=' by default.
+     */
+    char delimiter;
+
+    /**
+     * The character, when appearing at the end of a line, continues that line.
+     * This is '\' by default.
+     */
+    // char continuation; // TODO: line continuation in properties
+
+    /**
+     * The first comment character.
+     * This is '#' by default.
+     */
+    char comment1;
+
+    /**
+     * The second comment character.
+     * This is not set by default.
+     */
+    char comment2;
+
+    /**
+     * The third comment character.
+     * This is not set by default.
+     */
+    char comment3;
+};
+
+/**
+ * Typedef for the properties config.
+ */
+typedef struct cx_properties_config_s CxPropertiesConfig;
+
+/**
+ * Default properties configuration.
+ */
+extern const CxPropertiesConfig cx_properties_config_default;
+
+/**
+ * Status codes for the properties interface.
+ */
+enum cx_properties_status {
+    /**
+     * Everything is fine.
+     */
+    CX_PROPERTIES_NO_ERROR,
+    /**
+     * The input buffer does not contain more data.
+     */
+    CX_PROPERTIES_NO_DATA,
+    /**
+     * The input ends unexpectedly.
+     *
+     * This either happens when the last line does not terminate with a line
+     * break, or when the input ends with a parsed key but no value.
+     */
+    CX_PROPERTIES_INCOMPLETE_DATA,
+    /**
+     * Not used as a status and never returned by any function.
+     *
+     * You can use this enumerator to check for all "good" status results
+     * by checking if the status is less than @c CX_PROPERTIES_OK.
+     *
+     * A "good" status means, that you can refill data and continue parsing.
+     */
+    CX_PROPERTIES_OK,
+    /**
+     * Input buffer is @c NULL.
+     */
+    CX_PROPERTIES_NULL_INPUT,
+    /**
+     * The line contains a delimiter, but no key.
+     */
+    CX_PROPERTIES_INVALID_EMPTY_KEY,
+    /**
+     * The line contains data, but no delimiter.
+     */
+    CX_PROPERTIES_INVALID_MISSING_DELIMITER,
+    /**
+     * More internal buffer was needed, but could not be allocated.
+     */
+    CX_PROPERTIES_BUFFER_ALLOC_FAILED,
+    /**
+     * Initializing the properties source failed.
+     *
+     * @see cx_properties_read_init_func
+     */
+    CX_PROPERTIES_READ_INIT_FAILED,
+    /**
+     * Reading from a properties source failed.
+     *
+     * @see cx_properties_read_func
+     */
+    CX_PROPERTIES_READ_FAILED,
+    /**
+     * Sinking a k/v-pair failed.
+     *
+     * @see cx_properties_sink_func
+     */
+    CX_PROPERTIES_SINK_FAILED,
+};
+
+/**
+ * Typedef for the properties status enum.
+ */
+typedef enum cx_properties_status CxPropertiesStatus;
+
+/**
+ * Interface for working with properties data.
+ */
+struct cx_properties_s {
+    /**
+     * The configuration.
+     */
+    CxPropertiesConfig config;
+
+    /**
+     * The text input buffer.
+     */
+    CxBuffer input;
+
+    /**
+     * Internal buffer.
+     */
+    CxBuffer buffer;
+};
+
+/**
+ * Typedef for the properties interface.
+ */
+typedef struct cx_properties_s CxProperties;
+
+
+/**
+ * Typedef for a properties sink.
+ */
+typedef struct cx_properties_sink_s CxPropertiesSink;
+
+/**
+ * A function that consumes a k/v-pair in a sink.
+ *
+ * The sink could be e.g. a map and the sink function would be calling
+ * a map function to store the k/v-pair.
+ *
+ * @param prop the properties interface that wants to sink a k/v-pair
+ * @param sink the sink
+ * @param key the key
+ * @param value the value
+ * @retval zero success
+ * @retval non-zero sinking the k/v-pair failed
+ */
+cx_attr_nonnull
+typedef int(*cx_properties_sink_func)(
+        CxProperties *prop,
+        CxPropertiesSink *sink,
+        cxstring key,
+        cxstring value
+);
+
+/**
+ * Defines a sink for k/v-pairs.
+ */
+struct cx_properties_sink_s {
+    /**
+     * The sink object.
+     */
+    void *sink;
+    /**
+     * Optional custom data.
+     */
+    void *data;
+    /**
+     * A function for consuming k/v-pairs into the sink.
+     */
+    cx_properties_sink_func sink_func;
+};
+
+
+/**
+ * Typedef for a properties source.
+ */
+typedef struct cx_properties_source_s CxPropertiesSource;
+
+/**
+ * A function that reads data from a source.
+ *
+ * When the source is depleted, implementations SHALL provide an empty
+ * string in the @p target and return zero.
+ * A non-zero return value is only permitted in case of an error.
+ *
+ * The meaning of the optional parameters is implementation-dependent.
+ *
+ * @param prop the properties interface that wants to read from the source
+ * @param src the source
+ * @param target a string buffer where the read data shall be stored
+ * @retval zero success
+ * @retval non-zero reading the data failed
+ */
+cx_attr_nonnull
+typedef int(*cx_properties_read_func)(
+        CxProperties *prop,
+        CxPropertiesSource *src,
+        cxstring *target
+);
+
+/**
+ * A function that may initialize additional memory for the source.
+ *
+ * @param prop the properties interface that wants to read from the source
+ * @param src the source
+ * @retval zero initialization was successful
+ * @retval non-zero otherwise
+ */
+cx_attr_nonnull
+typedef int(*cx_properties_read_init_func)(
+        CxProperties *prop,
+        CxPropertiesSource *src
+);
+
+/**
+ * A function that cleans memory initialized by the read_init_func.
+ *
+ * @param prop the properties interface that wants to read from the source
+ * @param src the source
+ */
+cx_attr_nonnull
+typedef void(*cx_properties_read_clean_func)(
+        CxProperties *prop,
+        CxPropertiesSource *src
+);
+
+/**
+ * Defines a properties source.
+ */
+struct cx_properties_source_s {
+    /**
+     * The source object.
+     *
+     * For example a file stream or a string.
+     */
+    void *src;
+    /**
+     * Optional additional data pointer.
+     */
+    void *data_ptr;
+    /**
+     * Optional size information.
+     */
+    size_t data_size;
+    /**
+     * A function that reads data from the source.
+     */
+    cx_properties_read_func read_func;
+    /**
+     * Optional function that may prepare the source for reading data.
+     */
+    cx_properties_read_init_func read_init_func;
+    /**
+     * Optional function that cleans additional memory allocated by the
+     * read_init_func.
+     */
+    cx_properties_read_clean_func read_clean_func;
+};
+
+/**
+ * Initialize a properties interface.
+ *
+ * @param prop the properties interface
+ * @param config the properties configuration
+ * @see cxPropertiesInitDefault()
+ */
+cx_attr_nonnull
+void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
+
+/**
+ * Destroys the properties interface.
+ *
+ * @note Even when you are certain that you did not use the interface in a
+ * way that caused a memory allocation, you should call this function anyway.
+ * Future versions of the library might add features that need additional memory
+ * and you really don't want to search the entire code where you might need
+ * add call to this function.
+ *
+ * @param prop the properties interface
+ */
+cx_attr_nonnull
+void cxPropertiesDestroy(CxProperties *prop);
+
+/**
+ * Destroys and re-initializes the properties interface.
+ *
+ * You might want to use this, to reset the parser after
+ * encountering a syntax error.
+ *
+ * @param prop the properties interface
+ */
+cx_attr_nonnull
+static inline void cxPropertiesReset(CxProperties *prop) {
+    CxPropertiesConfig config = prop->config;
+    cxPropertiesDestroy(prop);
+    cxPropertiesInit(prop, config);
+}
+
+/**
+ * Initialize a properties parser with the default configuration.
+ *
+ * @param prop (@c CxProperties*) the properties interface
+ * @see cxPropertiesInit()
+ */
+#define cxPropertiesInitDefault(prop) \
+    cxPropertiesInit(prop, cx_properties_config_default)
+
+/**
+ * Fills the input buffer with data.
+ *
+ * After calling this function, you can parse the data by calling
+ * cxPropertiesNext().
+ *
+ * @remark The properties interface tries to avoid allocations.
+ * When you use this function and cxPropertiesNext() interleaving,
+ * no allocations are performed. However, you must not free the
+ * pointer to the data in that case. When you invoke the fill
+ * function more than once before calling cxPropertiesNext(),
+ * the additional data is appended - inevitably leading to
+ * an allocation of a new buffer and copying the previous contents.
+ *
+ * @param prop the properties interface
+ * @param buf a pointer to the data
+ * @param len the length of the data
+ * @retval zero success
+ * @retval non-zero a memory allocation was necessary but failed
+ * @see cxPropertiesFill()
+ */
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
+int cxPropertiesFilln(
+        CxProperties *prop,
+        const char *buf,
+        size_t len
+);
+
+#ifdef __cplusplus
+} // extern "C"
+cx_attr_nonnull
+static inline int cxPropertiesFill(
+        CxProperties *prop,
+        cxstring str
+) {
+    return cxPropertiesFilln(prop, str.ptr, str.length);
+}
+
+cx_attr_nonnull
+static inline int cxPropertiesFill(
+        CxProperties *prop,
+        cxmutstr str
+) {
+    return cxPropertiesFilln(prop, str.ptr, str.length);
+}
+
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
+static inline int cxPropertiesFill(
+        CxProperties *prop,
+        const char *str
+) {
+    return cxPropertiesFilln(prop, str, strlen(str));
+}
+
+extern "C" {
+#else // __cplusplus
+/**
+ * Fills the input buffer with data.
+ *
+ * After calling this function, you can parse the data by calling
+ * cxPropertiesNext().
+ *
+ * @attention The properties interface tries to avoid allocations.
+ * When you use this function and cxPropertiesNext() interleaving,
+ * no allocations are performed. However, you must not free the
+ * pointer to the data in that case. When you invoke the fill
+ * function more than once before calling cxPropertiesNext(),
+ * the additional data is appended - inevitably leading to
+ * an allocation of a new buffer and copying the previous contents.
+ *
+ * @param prop the properties interface
+ * @param str the text to fill in
+ * @retval zero success
+ * @retval non-zero a memory allocation was necessary but failed
+ * @see cxPropertiesFilln()
+ */
+#define cxPropertiesFill(prop, str) _Generic((str), \
+    cxstring: cx_properties_fill_cxstr,             \
+    cxmutstr: cx_properties_fill_mutstr,            \
+    char*: cx_properties_fill_str,                  \
+    const char*: cx_properties_fill_str)            \
+    (prop, str)
+
+/**
+ * @copydoc cxPropertiesFill()
+ */
+cx_attr_nonnull
+static inline int cx_properties_fill_cxstr(
+        CxProperties *prop,
+        cxstring str
+) {
+    return cxPropertiesFilln(prop, str.ptr, str.length);
+}
+
+/**
+ * @copydoc cxPropertiesFill()
+ */
+cx_attr_nonnull
+static inline int cx_properties_fill_mutstr(
+        CxProperties *prop,
+        cxmutstr str
+) {
+    return cxPropertiesFilln(prop, str.ptr, str.length);
+}
+
+/**
+ * @copydoc cxPropertiesFill()
+ */
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
+static inline int cx_properties_fill_str(
+        CxProperties *prop,
+        const char *str
+) {
+    return cxPropertiesFilln(prop, str, strlen(str));
+}
+#endif
+
+/**
+ * Specifies stack memory that shall be used as internal buffer.
+ *
+ * @param prop the properties interface
+ * @param buf a pointer to stack memory
+ * @param capacity the capacity of the stack memory
+ */
+cx_attr_nonnull
+void cxPropertiesUseStack(
+        CxProperties *prop,
+        char *buf,
+        size_t capacity
+);
+
+/**
+ * Retrieves the next key/value-pair.
+ *
+ * This function returns zero as long as there are key/value-pairs found.
+ * If no more key/value-pairs are found, #CX_PROPERTIES_NO_DATA is returned.
+ *
+ * When an incomplete line is encountered, #CX_PROPERTIES_INCOMPLETE_DATA is
+ * returned, and you can add more data with #cxPropertiesFill().
+ *
+ * @remark The incomplete line will be stored in an internal buffer, which is
+ * allocated on the heap, by default. If you want to avoid allocations,
+ * you can specify sufficient space with cxPropertiesUseStack() after
+ * initialization with cxPropertiesInit().
+ *
+ * @attention The returned strings will point into a buffer that might not be
+ * available later. It is strongly recommended to copy the strings for further
+ * use.
+ *
+ * @param prop the properties interface
+ * @param key a pointer to the cxstring that shall contain the property name
+ * @param value a pointer to the cxstring that shall contain the property value
+ * @retval CX_PROPERTIES_NO_ERROR (zero) a key/value pair was found
+ * @retval CX_PROPERTIES_NO_DATA there is no (more) data in the input buffer
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA the data in the input buffer is incomplete
+ * (fill more data and try again)
+ * @retval CX_PROPERTIES_NULL_INPUT the input buffer was never filled
+ * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
+ * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
+ * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+CxPropertiesStatus cxPropertiesNext(
+        CxProperties *prop,
+        cxstring *key,
+        cxstring *value
+);
+
+/**
+ * Creates a properties sink for an UCX map.
+ *
+ * The values stored in the map will be pointers to strings allocated
+ * by #cx_strdup_a().
+ * The default stdlib allocator will be used, unless you specify a custom
+ * allocator in the optional @c data of the sink.
+ *
+ * @param map the map that shall consume the k/v-pairs.
+ * @return the sink
+ * @see cxPropertiesLoad()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+CxPropertiesSink cxPropertiesMapSink(CxMap *map);
+
+/**
+ * Creates a properties source based on an UCX string.
+ *
+ * @param str the string
+ * @return the properties source
+ * @see cxPropertiesLoad()
+ */
+cx_attr_nodiscard
+CxPropertiesSource cxPropertiesStringSource(cxstring str);
+
+/**
+ * Creates a properties source based on C string with the specified length.
+ *
+ * @param str the string
+ * @param len the length
+ * @return the properties source
+ * @see cxPropertiesLoad()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_access_r(1, 2)
+CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len);
+
+/**
+ * Creates a properties source based on a C string.
+ *
+ * The length will be determined with strlen(), so the string MUST be
+ * zero-terminated.
+ *
+ * @param str the string
+ * @return the properties source
+ * @see cxPropertiesLoad()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
+CxPropertiesSource cxPropertiesCstrSource(const char *str);
+
+/**
+ * Creates a properties source based on an FILE.
+ *
+ * @param file the file
+ * @param chunk_size how many bytes may be read in one operation
+ *
+ * @return the properties source
+ * @see cxPropertiesLoad()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_access_r(1)
+CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size);
+
+
+/**
+ * Loads properties data from a source and transfers it to a sink.
+ *
+ * This function tries to read as much data from the source as possible.
+ * When the source was completely consumed and at least on k/v-pair was found,
+ * the return value will be #CX_PROPERTIES_NO_ERROR.
+ * When the source was consumed but no k/v-pairs were found, the return value
+ * will be #CX_PROPERTIES_NO_DATA.
+ * The other result codes apply, according to their description.
+ *
+ * @param prop the properties interface
+ * @param sink the sink
+ * @param source the source
+ * @retval CX_PROPERTIES_NO_ERROR (zero) a key/value pair was found
+ * @retval CX_PROPERTIES_READ_INIT_FAILED initializing the source failed
+ * @retval CX_PROPERTIES_READ_FAILED reading from the source failed
+ * @retval CX_PROPERTIES_SINK_FAILED sinking the properties into the sink failed
+ * @retval CX_PROPERTIES_NO_DATA the source did not provide any key/value pairs
+ * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
+ * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
+ * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
+ */
+cx_attr_nonnull
+CxPropertiesStatus cxPropertiesLoad(
+        CxProperties *prop,
+        CxPropertiesSink sink,
+        CxPropertiesSource source
+);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // UCX_PROPERTIES_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ucx/cx/streams.h	Sun Jan 05 22:00:39 2025 +0100
@@ -0,0 +1,135 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file streams.h
+ *
+ * @brief Utility functions for data streams.
+ *
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
+ */
+
+#ifndef UCX_STREAMS_H
+#define UCX_STREAMS_H
+
+#include "common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Reads data from a stream and writes it to another stream.
+ *
+ * @param src the source stream
+ * @param dest the destination stream
+ * @param rfnc the read function
+ * @param wfnc the write function
+ * @param buf a pointer to the copy buffer or @c NULL if a buffer
+ * shall be implicitly created on the heap
+ * @param bufsize the size of the copy buffer - if @p buf is @c NULL you can
+ * set this to zero to let the implementation decide
+ * @param n the maximum number of bytes that shall be copied.
+ * If this is larger than @p bufsize, the content is copied over multiple
+ * iterations.
+ * @return the total number of bytes copied
+ */
+cx_attr_nonnull_arg(1, 2, 3, 4)
+cx_attr_access_r(1)
+cx_attr_access_w(2)
+cx_attr_access_w(5)
+size_t cx_stream_bncopy(
+        void *src,
+        void *dest,
+        cx_read_func rfnc,
+        cx_write_func wfnc,
+        char *buf,
+        size_t bufsize,
+        size_t n
+);
+
+/**
+ * Reads data from a stream and writes it to another stream.
+ *
+ * @param src (@c void*) the source stream
+ * @param dest (@c void*) the destination stream
+ * @param rfnc (@c cx_read_func) the read function
+ * @param wfnc (@c cx_write_func) the write function
+ * @param buf (@c char*) a pointer to the copy buffer or @c NULL if a buffer
+ * shall be implicitly created on the heap
+ * @param bufsize (@c size_t) the size of the copy buffer - if @p buf is
+ * @c NULL you can set this to zero to let the implementation decide
+ * @return total number of bytes copied
+ */
+#define cx_stream_bcopy(src, dest, rfnc, wfnc, buf, bufsize) \
+    cx_stream_bncopy(src, dest, rfnc, wfnc, buf, bufsize, SIZE_MAX)
+
+/**
+ * Reads data from a stream and writes it to another stream.
+ *
+ * The data is temporarily stored in a stack allocated buffer.
+ *
+ * @param src the source stream
+ * @param dest the destination stream
+ * @param rfnc the read function
+ * @param wfnc the write function
+ * @param n the maximum number of bytes that shall be copied.
+ * @return total number of bytes copied
+ */
+cx_attr_nonnull
+cx_attr_access_r(1)
+cx_attr_access_w(2)
+size_t cx_stream_ncopy(
+        void *src,
+        void *dest,
+        cx_read_func rfnc,
+        cx_write_func wfnc,
+        size_t n
+);
+
+/**
+ * Reads data from a stream and writes it to another stream.
+ *
+ * The data is temporarily stored in a stack allocated buffer.
+ *
+ * @param src (@c void*) the source stream
+ * @param dest (@c void*) the destination stream
+ * @param rfnc (@c cx_read_func) the read function
+ * @param wfnc (@c cx_write_func) the write function
+ * @return total number of bytes copied
+ */
+#define cx_stream_copy(src, dest, rfnc, wfnc) \
+    cx_stream_ncopy(src, dest, rfnc, wfnc, SIZE_MAX)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // UCX_STREAMS_H
--- a/ucx/cx/string.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/string.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file string.h
- * \brief Strings that know their length.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file string.h
+ * @brief Strings that know their length.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_STRING_H
@@ -42,7 +42,7 @@
 /**
  * The maximum length of the "needle" in cx_strstr() that can use SBO.
  */
-extern unsigned const cx_strstr_sbo_size;
+extern const unsigned cx_strstr_sbo_size;
 
 /**
  * The UCX string structure.
@@ -50,7 +50,7 @@
 struct cx_mutstr_s {
     /**
      * A pointer to the string.
-     * \note The string is not necessarily \c NULL terminated.
+     * @note The string is not necessarily @c NULL terminated.
      * Always use the length.
      */
     char *ptr;
@@ -69,7 +69,7 @@
 struct cx_string_s {
     /**
      * A pointer to the immutable string.
-     * \note The string is not necessarily \c NULL terminated.
+     * @note The string is not necessarily @c NULL terminated.
      * Always use the length.
      */
     const char *ptr;
@@ -148,7 +148,7 @@
 /**
  * A literal initializer for an UCX string structure.
  *
- * The argument MUST be a string (const char*) \em literal.
+ * The argument MUST be a string (const char*) @em literal.
  *
  * @param literal the string literal
  */
@@ -160,9 +160,9 @@
 /**
  * Wraps a mutable string that must be zero-terminated.
  *
- * The length is implicitly inferred by using a call to \c strlen().
+ * The length is implicitly inferred by using a call to @c strlen().
  *
- * \note the wrapped string will share the specified pointer to the string.
+ * @note the wrapped string will share the specified pointer to the string.
  * If you do want a copy, use cx_strdup() on the return value of this function.
  *
  * If you need to wrap a constant string, use cx_str().
@@ -172,26 +172,29 @@
  *
  * @see cx_mutstrn()
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
 cxmutstr cx_mutstr(char *cstring);
 
 /**
  * Wraps a string that does not need to be zero-terminated.
  *
- * The argument may be \c NULL if the length is zero.
+ * The argument may be @c NULL if the length is zero.
  *
- * \note the wrapped string will share the specified pointer to the string.
+ * @note the wrapped string will share the specified pointer to the string.
  * If you do want a copy, use cx_strdup() on the return value of this function.
  *
  * If you need to wrap a constant string, use cx_strn().
  *
- * @param cstring  the string to wrap (or \c NULL, only if the length is zero)
+ * @param cstring  the string to wrap (or @c NULL, only if the length is zero)
  * @param length   the length of the string
  * @return the wrapped string
  *
  * @see cx_mutstr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_access_rw(1, 2)
 cxmutstr cx_mutstrn(
         char *cstring,
         size_t length
@@ -200,9 +203,9 @@
 /**
  * Wraps a string that must be zero-terminated.
  *
- * The length is implicitly inferred by using a call to \c strlen().
+ * The length is implicitly inferred by using a call to @c strlen().
  *
- * \note the wrapped string will share the specified pointer to the string.
+ * @note the wrapped string will share the specified pointer to the string.
  * If you do want a copy, use cx_strdup() on the return value of this function.
  *
  * If you need to wrap a non-constant string, use cx_mutstr().
@@ -212,72 +215,112 @@
  *
  * @see cx_strn()
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
 cxstring cx_str(const char *cstring);
 
 
 /**
  * Wraps a string that does not need to be zero-terminated.
  *
- * The argument may be \c NULL if the length is zero.
+ * The argument may be @c NULL if the length is zero.
  *
- * \note the wrapped string will share the specified pointer to the string.
+ * @note the wrapped string will share the specified pointer to the string.
  * If you do want a copy, use cx_strdup() on the return value of this function.
  *
  * If you need to wrap a non-constant string, use cx_mutstrn().
  *
- * @param cstring  the string to wrap (or \c NULL, only if the length is zero)
+ * @param cstring  the string to wrap (or @c NULL, only if the length is zero)
  * @param length   the length of the string
  * @return the wrapped string
  *
  * @see cx_str()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_access_r(1, 2)
 cxstring cx_strn(
         const char *cstring,
         size_t length
 );
 
+#ifdef __cplusplus
+} // extern "C"
+cx_attr_nodiscard
+static inline cxstring cx_strcast(cxmutstr str) {
+    return cx_strn(str.ptr, str.length);
+}
+cx_attr_nodiscard
+static inline cxstring cx_strcast(cxstring str) {
+    return str;
+}
+extern "C" {
+#else
+/**
+ * Internal function, do not use.
+ * @param str
+ * @return
+ * @see cx_strcast()
+ */
+cx_attr_nodiscard
+static inline cxstring cx_strcast_m(cxmutstr str) {
+    return (cxstring) {str.ptr, str.length};
+}
+/**
+ * Internal function, do not use.
+ * @param str
+ * @return
+ * @see cx_strcast()
+ */
+cx_attr_nodiscard
+static inline cxstring cx_strcast_c(cxstring str) {
+    return str;
+}
+
 /**
 * Casts a mutable string to an immutable string.
 *
-* \note This is not seriously a cast. Instead you get a copy
+* Does nothing for already immutable strings.
+*
+* @note This is not seriously a cast. Instead, you get a copy
 * of the struct with the desired pointer type. Both structs still
 * point to the same location, though!
 *
-* @param str the mutable string to cast
-* @return an immutable copy of the string pointer
+* @param str (@c cxstring or @c cxmutstr) the string to cast
+* @return (@c cxstring) an immutable copy of the string pointer
 */
-__attribute__((__warn_unused_result__))
-cxstring cx_strcast(cxmutstr str);
+#define cx_strcast(str) _Generic((str), \
+        cxmutstr: cx_strcast_m, \
+        cxstring: cx_strcast_c) \
+        (str)
+#endif
 
 /**
- * Passes the pointer in this string to \c free().
+ * Passes the pointer in this string to @c free().
  *
- * The pointer in the struct is set to \c NULL and the length is set to zero.
+ * The pointer in the struct is set to @c NULL and the length is set to zero.
  *
- * \note There is no implementation for cxstring, because it is unlikely that
+ * @note There is no implementation for cxstring, because it is unlikely that
  * you ever have a <code>const char*</code> you are really supposed to free.
  * If you encounter such situation, you should double-check your code.
  *
  * @param str the string to free
  */
-__attribute__((__nonnull__))
 void cx_strfree(cxmutstr *str);
 
 /**
  * Passes the pointer in this string to the allocators free function.
  *
- * The pointer in the struct is set to \c NULL and the length is set to zero.
+ * The pointer in the struct is set to @c NULL and the length is set to zero.
  *
- * \note There is no implementation for cxstring, because it is unlikely that
+ * @note There is no implementation for cxstring, because it is unlikely that
  * you ever have a <code>const char*</code> you are really supposed to free.
  * If you encounter such situation, you should double-check your code.
  *
  * @param alloc the allocator
  * @param str the string to free
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull_arg(1)
 void cx_strfree_a(
         const CxAllocator *alloc,
         cxmutstr *str
@@ -285,15 +328,17 @@
 
 /**
  * Returns the accumulated length of all specified strings.
+ * 
+ * If this sum overflows, errno is set to EOVERFLOW.
  *
- * \attention if the count argument is larger than the number of the
+ * @attention if the count argument is larger than the number of the
  * specified strings, the behavior is undefined.
  *
  * @param count    the total number of specified strings
  * @param ...      all strings
  * @return the accumulated length of all strings
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 size_t cx_strlen(
         size_t count,
         ...
@@ -303,21 +348,26 @@
  * Concatenates strings.
  *
  * The resulting string will be allocated by the specified allocator.
- * So developers \em must pass the return value to cx_strfree_a() eventually.
+ * So developers @em must pass the return value to cx_strfree_a() eventually.
  *
- * If \p str already contains a string, the memory will be reallocated and
+ * If @p str already contains a string, the memory will be reallocated and
  * the other strings are appended. Otherwise, new memory is allocated.
  *
- * \note It is guaranteed that there is only one allocation.
+ * If memory allocation fails, the pointer in the returned string will
+ * be @c NULL. Depending on the allocator, @c errno might be set.
+ *
+ * @note It is guaranteed that there is only one allocation for the
+ * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
  * @param alloc the allocator to use
  * @param str   the string the other strings shall be concatenated to
  * @param count the number of the other following strings to concatenate
- * @param ...   all other strings
+ * @param ...   all other UCX strings
  * @return the concatenated string
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 cxmutstr cx_strcat_ma(
         const CxAllocator *alloc,
         cxmutstr str,
@@ -329,15 +379,19 @@
  * Concatenates strings and returns a new string.
  *
  * The resulting string will be allocated by the specified allocator.
- * So developers \em must pass the return value to cx_strfree_a() eventually.
+ * So developers @em must pass the return value to cx_strfree_a() eventually.
  *
- * \note It is guaranteed that there is only one allocation.
+* If memory allocation fails, the pointer in the returned string will
+ * be @c NULL. Depending on the allocator, @c errno might be set.
+ *
+ * @note It is guaranteed that there is only one allocation for the
+ * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
- * @param alloc the allocator to use
- * @param count the number of the other following strings to concatenate
- * @param ...   all other strings
- * @return the concatenated string
+ * @param alloc (@c CxAllocator*) the allocator to use
+ * @param count (@c size_t) the number of the other following strings to concatenate
+ * @param ...   all other UCX strings
+ * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat_a(alloc, count, ...) \
 cx_strcat_ma(alloc, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
@@ -345,15 +399,19 @@
 /**
  * Concatenates strings and returns a new string.
  *
- * The resulting string will be allocated by standard \c malloc().
- * So developers \em must pass the return value to cx_strfree() eventually.
+ * The resulting string will be allocated by standard @c malloc().
+ * So developers @em must pass the return value to cx_strfree() eventually.
  *
- * \note It is guaranteed that there is only one allocation.
+* If memory allocation fails, the pointer in the returned string will
+ * be @c NULL and @c errno might be set.
+ *
+ * @note It is guaranteed that there is only one allocation for the
+ * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
- * @param count   the number of the other following strings to concatenate
- * @param ...     all other strings
- * @return the concatenated string
+ * @param count (@c size_t) the number of the other following strings to concatenate
+ * @param ... all other UCX strings
+ * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat(count, ...) \
 cx_strcat_ma(cxDefaultAllocator, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
@@ -361,19 +419,23 @@
 /**
  * Concatenates strings.
  *
- * The resulting string will be allocated by standard \c malloc().
- * So developers \em must pass the return value to cx_strfree() eventually.
+ * The resulting string will be allocated by standard @c malloc().
+ * So developers @em must pass the return value to cx_strfree() eventually.
  *
- * If \p str already contains a string, the memory will be reallocated and
+ * If @p str already contains a string, the memory will be reallocated and
  * the other strings are appended. Otherwise, new memory is allocated.
  *
- * \note It is guaranteed that there is only one allocation.
+* If memory allocation fails, the pointer in the returned string will
+ * be @c NULL and @c errno might be set.
+ *
+ * @note It is guaranteed that there is only one allocation for the
+ * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
- * @param str     the string the other strings shall be concatenated to
- * @param count   the number of the other following strings to concatenate
- * @param ...     all other strings
- * @return the concatenated string
+ * @param str (@c cxmutstr) the string the other strings shall be concatenated to
+ * @param count (@c size_t) the number of the other following strings to concatenate
+ * @param ... all other strings
+ * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat_m(str, count, ...) \
 cx_strcat_ma(cxDefaultAllocator, str, count, __VA_ARGS__)
@@ -381,19 +443,19 @@
 /**
  * Returns a substring starting at the specified location.
  *
- * \attention the new string references the same memory area as the
- * input string and is usually \em not zero-terminated.
+ * @attention the new string references the same memory area as the
+ * input string and is usually @em not zero-terminated.
  * Use cx_strdup() to get a copy.
  *
  * @param string input string
  * @param start  start location of the substring
- * @return a substring of \p string starting at \p start
+ * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubsl()
  * @see cx_strsubs_m()
  * @see cx_strsubsl_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strsubs(
         cxstring string,
         size_t start
@@ -402,23 +464,23 @@
 /**
  * Returns a substring starting at the specified location.
  *
- * The returned string will be limited to \p length bytes or the number
- * of bytes available in \p string, whichever is smaller.
+ * The returned string will be limited to @p length bytes or the number
+ * of bytes available in @p string, whichever is smaller.
  *
- * \attention the new string references the same memory area as the
- * input string and is usually \em not zero-terminated.
+ * @attention the new string references the same memory area as the
+ * input string and is usually @em not zero-terminated.
  * Use cx_strdup() to get a copy.
  *
  * @param string input string
  * @param start  start location of the substring
  * @param length the maximum length of the returned string
- * @return a substring of \p string starting at \p start
+ * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubs()
  * @see cx_strsubs_m()
  * @see cx_strsubsl_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strsubsl(
         cxstring string,
         size_t start,
@@ -428,19 +490,19 @@
 /**
  * Returns a substring starting at the specified location.
  *
- * \attention the new string references the same memory area as the
- * input string and is usually \em not zero-terminated.
+ * @attention the new string references the same memory area as the
+ * input string and is usually @em not zero-terminated.
  * Use cx_strdup() to get a copy.
  *
  * @param string input string
  * @param start  start location of the substring
- * @return a substring of \p string starting at \p start
+ * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubsl_m()
  * @see cx_strsubs()
  * @see cx_strsubsl()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strsubs_m(
         cxmutstr string,
         size_t start
@@ -449,23 +511,23 @@
 /**
  * Returns a substring starting at the specified location.
  *
- * The returned string will be limited to \p length bytes or the number
- * of bytes available in \p string, whichever is smaller.
+ * The returned string will be limited to @p length bytes or the number
+ * of bytes available in @p string, whichever is smaller.
  *
- * \attention the new string references the same memory area as the
- * input string and is usually \em not zero-terminated.
+ * @attention the new string references the same memory area as the
+ * input string and is usually @em not zero-terminated.
  * Use cx_strdup() to get a copy.
  *
  * @param string input string
  * @param start  start location of the substring
  * @param length the maximum length of the returned string
- * @return a substring of \p string starting at \p start
+ * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubs_m()
  * @see cx_strsubs()
  * @see cx_strsubsl()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strsubsl_m(
         cxmutstr string,
         size_t start,
@@ -480,11 +542,11 @@
  *
  * @param string the string where to locate the character
  * @param chr    the character to locate
- * @return       a substring starting at the first location of \p chr
+ * @return       a substring starting at the first location of @p chr
  *
  * @see cx_strchr_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strchr(
         cxstring string,
         int chr
@@ -498,11 +560,11 @@
  *
  * @param string the string where to locate the character
  * @param chr    the character to locate
- * @return       a substring starting at the first location of \p chr
+ * @return       a substring starting at the first location of @p chr
  *
  * @see cx_strchr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strchr_m(
         cxmutstr string,
         int chr
@@ -516,11 +578,11 @@
  *
  * @param string the string where to locate the character
  * @param chr    the character to locate
- * @return       a substring starting at the last location of \p chr
+ * @return       a substring starting at the last location of @p chr
  *
  * @see cx_strrchr_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strrchr(
         cxstring string,
         int chr
@@ -534,11 +596,11 @@
  *
  * @param string the string where to locate the character
  * @param chr    the character to locate
- * @return       a substring starting at the last location of \p chr
+ * @return       a substring starting at the last location of @p chr
  *
  * @see cx_strrchr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strrchr_m(
         cxmutstr string,
         int chr
@@ -548,19 +610,19 @@
  * Returns a substring starting at the location of the first occurrence of the
  * specified string.
  *
- * If \p haystack does not contain \p needle, an empty string is returned.
+ * If @p haystack does not contain @p needle, an empty string is returned.
  *
- * If \p needle is an empty string, the complete \p haystack is
+ * If @p needle is an empty string, the complete @p haystack is
  * returned.
  *
  * @param haystack the string to be scanned
  * @param needle  string containing the sequence of characters to match
  * @return       a substring starting at the first occurrence of
- *               \p needle, or an empty string, if the sequence is not
+ *               @p needle, or an empty string, if the sequence is not
  *               contained
  * @see cx_strstr_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strstr(
         cxstring haystack,
         cxstring needle
@@ -570,19 +632,19 @@
  * Returns a substring starting at the location of the first occurrence of the
  * specified string.
  *
- * If \p haystack does not contain \p needle, an empty string is returned.
+ * If @p haystack does not contain @p needle, an empty string is returned.
  *
- * If \p needle is an empty string, the complete \p haystack is
+ * If @p needle is an empty string, the complete @p haystack is
  * returned.
  *
  * @param haystack the string to be scanned
  * @param needle  string containing the sequence of characters to match
  * @return       a substring starting at the first occurrence of
- *               \p needle, or an empty string, if the sequence is not
+ *               @p needle, or an empty string, if the sequence is not
  *               contained
  * @see cx_strstr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strstr_m(
         cxmutstr haystack,
         cxstring needle
@@ -591,16 +653,18 @@
 /**
  * Splits a given string using a delimiter string.
  *
- * \note The resulting array contains strings that point to the source
- * \p string. Use cx_strdup() to get copies.
+ * @note The resulting array contains strings that point to the source
+ * @p string. Use cx_strdup() to get copies.
  *
  * @param string the string to split
  * @param delim  the delimiter
  * @param limit the maximum number of split items
- * @param output a pre-allocated array of at least \p limit length
+ * @param output a pre-allocated array of at least @p limit length
  * @return the actual number of split items
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_access_w(4, 3)
 size_t cx_strsplit(
         cxstring string,
         cxstring delim,
@@ -611,13 +675,13 @@
 /**
  * Splits a given string using a delimiter string.
  *
- * The array pointed to by \p output will be allocated by \p allocator.
+ * The array pointed to by @p output will be allocated by @p allocator.
  *
- * \note The resulting array contains strings that point to the source
- * \p string. Use cx_strdup() to get copies.
+ * @note The resulting array contains strings that point to the source
+ * @p string. Use cx_strdup() to get copies.
  *
- * \attention If allocation fails, the \c NULL pointer will be written to
- * \p output and the number returned will be zero.
+ * @attention If allocation fails, the @c NULL pointer will be written to
+ * @p output and the number returned will be zero.
  *
  * @param allocator the allocator to use for allocating the resulting array
  * @param string the string to split
@@ -627,7 +691,9 @@
  * written to
  * @return the actual number of split items
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_access_w(5)
 size_t cx_strsplit_a(
         const CxAllocator *allocator,
         cxstring string,
@@ -640,16 +706,18 @@
 /**
  * Splits a given string using a delimiter string.
  *
- * \note The resulting array contains strings that point to the source
- * \p string. Use cx_strdup() to get copies.
+ * @note The resulting array contains strings that point to the source
+ * @p string. Use cx_strdup() to get copies.
  *
  * @param string the string to split
  * @param delim  the delimiter
  * @param limit the maximum number of split items
- * @param output a pre-allocated array of at least \p limit length
+ * @param output a pre-allocated array of at least @p limit length
  * @return the actual number of split items
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_access_w(4, 3)
 size_t cx_strsplit_m(
         cxmutstr string,
         cxstring delim,
@@ -660,13 +728,13 @@
 /**
  * Splits a given string using a delimiter string.
  *
- * The array pointed to by \p output will be allocated by \p allocator.
+ * The array pointed to by @p output will be allocated by @p allocator.
  *
- * \note The resulting array contains strings that point to the source
- * \p string. Use cx_strdup() to get copies.
+ * @note The resulting array contains strings that point to the source
+ * @p string. Use cx_strdup() to get copies.
  *
- * \attention If allocation fails, the \c NULL pointer will be written to
- * \p output and the number returned will be zero.
+ * @attention If allocation fails, the @c NULL pointer will be written to
+ * @p output and the number returned will be zero.
  *
  * @param allocator the allocator to use for allocating the resulting array
  * @param string the string to split
@@ -676,7 +744,9 @@
  * written to
  * @return the actual number of split items
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_access_w(5)
 size_t cx_strsplit_ma(
         const CxAllocator *allocator,
         cxmutstr string,
@@ -690,10 +760,10 @@
  *
  * @param s1 the first string
  * @param s2 the second string
- * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
- * than \p s2, zero if both strings equal
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 int cx_strcmp(
         cxstring s1,
         cxstring s2
@@ -704,10 +774,10 @@
  *
  * @param s1 the first string
  * @param s2 the second string
- * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
- * than \p s2, zero if both strings equal ignoring case
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal ignoring case
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 int cx_strcasecmp(
         cxstring s1,
         cxstring s2
@@ -720,10 +790,11 @@
  *
  * @param s1 the first string
  * @param s2 the second string
- * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
- * than \p s2, zero if both strings equal
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 int cx_strcmp_p(
         const void *s1,
         const void *s2
@@ -736,10 +807,11 @@
  *
  * @param s1 the first string
  * @param s2 the second string
- * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
- * than \p s2, zero if both strings equal ignoring case
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal ignoring case
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 int cx_strcasecmp_p(
         const void *s1,
         const void *s2
@@ -749,16 +821,17 @@
 /**
  * Creates a duplicate of the specified string.
  *
- * The new string will contain a copy allocated by \p allocator.
+ * The new string will contain a copy allocated by @p allocator.
  *
- * \note The returned string is guaranteed to be zero-terminated.
+ * @note The returned string is guaranteed to be zero-terminated.
  *
  * @param allocator the allocator to use
  * @param string the string to duplicate
  * @return a duplicate of the string
  * @see cx_strdup()
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 cxmutstr cx_strdup_a(
         const CxAllocator *allocator,
         cxstring string
@@ -768,12 +841,12 @@
  * Creates a duplicate of the specified string.
  *
  * The new string will contain a copy allocated by standard
- * \c malloc(). So developers \em must pass the return value to cx_strfree().
+ * @c malloc(). So developers @em must pass the return value to cx_strfree().
  *
- * \note The returned string is guaranteed to be zero-terminated.
+ * @note The returned string is guaranteed to be zero-terminated.
  *
- * @param string the string to duplicate
- * @return a duplicate of the string
+ * @param string (@c cxstring) the string to duplicate
+ * @return (@c cxmutstr) a duplicate of the string
  * @see cx_strdup_a()
  */
 #define cx_strdup(string) cx_strdup_a(cxDefaultAllocator, string)
@@ -782,13 +855,13 @@
 /**
  * Creates a duplicate of the specified string.
  *
- * The new string will contain a copy allocated by \p allocator.
+ * The new string will contain a copy allocated by @p allocator.
  *
- * \note The returned string is guaranteed to be zero-terminated.
+ * @note The returned string is guaranteed to be zero-terminated.
  *
- * @param allocator the allocator to use
- * @param string the string to duplicate
- * @return a duplicate of the string
+ * @param allocator (@c CxAllocator*) the allocator to use
+ * @param string (@c cxmutstr) the string to duplicate
+ * @return (@c cxmutstr) a duplicate of the string
  * @see cx_strdup_m()
  */
 #define cx_strdup_ma(allocator, string) cx_strdup_a(allocator, cx_strcast(string))
@@ -797,12 +870,12 @@
  * Creates a duplicate of the specified string.
  *
  * The new string will contain a copy allocated by standard
- * \c malloc(). So developers \em must pass the return value to cx_strfree().
+ * @c malloc(). So developers @em must pass the return value to cx_strfree().
  *
- * \note The returned string is guaranteed to be zero-terminated.
+ * @note The returned string is guaranteed to be zero-terminated.
  *
- * @param string the string to duplicate
- * @return a duplicate of the string
+ * @param string (@c cxmutstr) the string to duplicate
+ * @return (@c cxmutstr) a duplicate of the string
  * @see cx_strdup_ma()
  */
 #define cx_strdup_m(string) cx_strdup_a(cxDefaultAllocator, cx_strcast(string))
@@ -810,25 +883,25 @@
 /**
  * Omits leading and trailing spaces.
  *
- * \note the returned string references the same memory, thus you
- * must \em not free the returned memory.
+ * @note the returned string references the same memory, thus you
+ * must @em not free the returned memory.
  *
  * @param string the string that shall be trimmed
  * @return the trimmed string
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strtrim(cxstring string);
 
 /**
  * Omits leading and trailing spaces.
  *
- * \note the returned string references the same memory, thus you
- * must \em not free the returned memory.
+ * @note the returned string references the same memory, thus you
+ * must @em not free the returned memory.
  *
  * @param string the string that shall be trimmed
  * @return the trimmed string
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strtrim_m(cxmutstr string);
 
 /**
@@ -836,10 +909,10 @@
  *
  * @param string the string to check
  * @param prefix the prefix the string should have
- * @return \c true, if and only if the string has the specified prefix,
- * \c false otherwise
+ * @return @c true, if and only if the string has the specified prefix,
+ * @c false otherwise
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 bool cx_strprefix(
         cxstring string,
         cxstring prefix
@@ -850,10 +923,10 @@
  *
  * @param string the string to check
  * @param suffix the suffix the string should have
- * @return \c true, if and only if the string has the specified suffix,
- * \c false otherwise
+ * @return @c true, if and only if the string has the specified suffix,
+ * @c false otherwise
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 bool cx_strsuffix(
         cxstring string,
         cxstring suffix
@@ -864,10 +937,10 @@
  *
  * @param string the string to check
  * @param prefix the prefix the string should have
- * @return \c true, if and only if the string has the specified prefix,
- * \c false otherwise
+ * @return @c true, if and only if the string has the specified prefix,
+ * @c false otherwise
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 bool cx_strcaseprefix(
         cxstring string,
         cxstring prefix
@@ -878,10 +951,10 @@
  *
  * @param string the string to check
  * @param suffix the suffix the string should have
- * @return \c true, if and only if the string has the specified suffix,
- * \c false otherwise
+ * @return @c true, if and only if the string has the specified suffix,
+ * @c false otherwise
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 bool cx_strcasesuffix(
         cxstring string,
         cxstring suffix
@@ -911,9 +984,9 @@
  * Replaces a pattern in a string with another string.
  *
  * The pattern is taken literally and is no regular expression.
- * Replaces at most \p replmax occurrences.
+ * Replaces at most @p replmax occurrences.
  *
- * The returned string will be allocated by \p allocator and is guaranteed
+ * The returned string will be allocated by @p allocator and is guaranteed
  * to be zero-terminated.
  *
  * If allocation fails, or the input string is empty,
@@ -926,7 +999,8 @@
  * @param replmax maximum number of replacements
  * @return the resulting string after applying the replacements
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 cxmutstr cx_strreplacen_a(
         const CxAllocator *allocator,
         cxstring str,
@@ -939,19 +1013,19 @@
  * Replaces a pattern in a string with another string.
  *
  * The pattern is taken literally and is no regular expression.
- * Replaces at most \p replmax occurrences.
+ * Replaces at most @p replmax occurrences.
  *
- * The returned string will be allocated by \c malloc() and is guaranteed
+ * The returned string will be allocated by @c malloc() and is guaranteed
  * to be zero-terminated.
  *
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param str the string where replacements should be applied
- * @param pattern the pattern to search for
- * @param replacement the replacement string
- * @param replmax maximum number of replacements
- * @return the resulting string after applying the replacements
+ * @param str (@c cxstring) the string where replacements should be applied
+ * @param pattern (@c cxstring) the pattern to search for
+ * @param replacement (@c cxstring) the replacement string
+ * @param replmax (@c size_t) maximum number of replacements
+ * @return (@c cxmutstr) the resulting string after applying the replacements
  */
 #define cx_strreplacen(str, pattern, replacement, replmax) \
 cx_strreplacen_a(cxDefaultAllocator, str, pattern, replacement, replmax)
@@ -961,17 +1035,17 @@
  *
  * The pattern is taken literally and is no regular expression.
  *
- * The returned string will be allocated by \p allocator and is guaranteed
+ * The returned string will be allocated by @p allocator and is guaranteed
  * to be zero-terminated.
  *
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param allocator the allocator to use
- * @param str the string where replacements should be applied
- * @param pattern the pattern to search for
- * @param replacement the replacement string
- * @return the resulting string after applying the replacements
+ * @param allocator (@c CxAllocator*) the allocator to use
+ * @param str (@c cxstring) the string where replacements should be applied
+ * @param pattern (@c cxstring) the pattern to search for
+ * @param replacement (@c cxstring) the replacement string
+ * @return (@c cxmutstr) the resulting string after applying the replacements
  */
 #define cx_strreplace_a(allocator, str, pattern, replacement) \
 cx_strreplacen_a(allocator, str, pattern, replacement, SIZE_MAX)
@@ -980,18 +1054,18 @@
  * Replaces a pattern in a string with another string.
  *
  * The pattern is taken literally and is no regular expression.
- * Replaces at most \p replmax occurrences.
+ * Replaces at most @p replmax occurrences.
  *
- * The returned string will be allocated by \c malloc() and is guaranteed
+ * The returned string will be allocated by @c malloc() and is guaranteed
  * to be zero-terminated.
  *
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param str the string where replacements should be applied
- * @param pattern the pattern to search for
- * @param replacement the replacement string
- * @return the resulting string after applying the replacements
+ * @param str (@c cxstring) the string where replacements should be applied
+ * @param pattern (@c cxstring) the pattern to search for
+ * @param replacement (@c cxstring) the replacement string
+ * @return (@c cxmutstr) the resulting string after applying the replacements
  */
 #define cx_strreplace(str, pattern, replacement) \
 cx_strreplacen_a(cxDefaultAllocator, str, pattern, replacement, SIZE_MAX)
@@ -1004,7 +1078,7 @@
  * @param limit the maximum number of tokens that shall be returned
  * @return a new string tokenization context
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxStrtokCtx cx_strtok(
         cxstring str,
         cxstring delim,
@@ -1019,7 +1093,7 @@
 * @param limit the maximum number of tokens that shall be returned
 * @return a new string tokenization context
 */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxStrtokCtx cx_strtok_m(
         cxmutstr str,
         cxstring delim,
@@ -1036,7 +1110,9 @@
  * @return true if successful, false if the limit or the end of the string
  * has been reached
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_access_w(2)
 bool cx_strtok_next(
         CxStrtokCtx *ctx,
         cxstring *token
@@ -1054,7 +1130,9 @@
  * @return true if successful, false if the limit or the end of the string
  * has been reached
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_access_w(2)
 bool cx_strtok_next_m(
         CxStrtokCtx *ctx,
         cxmutstr *token
@@ -1067,13 +1145,407 @@
  * @param delim array of more delimiters
  * @param count number of elements in the array
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
 void cx_strtok_delim(
         CxStrtokCtx *ctx,
         const cxstring *delim,
         size_t count
 );
 
+/* ------------------------------------------------------------------------- *
+ *                string to number conversion functions                      *
+ * ------------------------------------------------------------------------- */
+
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtos_lc(cxstring str, short *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoi_lc(cxstring str, int *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtol_lc(cxstring str, long *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoll_lc(cxstring str, long long *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoi8_lc(cxstring str, int8_t *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoi16_lc(cxstring str, int16_t *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoi32_lc(cxstring str, int32_t *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoi64_lc(cxstring str, int64_t *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoz_lc(cxstring str, ssize_t *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtous_lc(cxstring str, unsigned short *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtou_lc(cxstring str, unsigned int *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoul_lc(cxstring str, unsigned long *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtoull_lc(cxstring str, unsigned long long *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtou8_lc(cxstring str, uint8_t *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtou16_lc(cxstring str, uint16_t *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtou32_lc(cxstring str, uint32_t *output, int base, const char *groupsep);
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtou64_lc(cxstring str, uint64_t *output, int base, const char *groupsep);
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtouz_lc(cxstring str, size_t *output, int base, const char *groupsep);
+
+/**
+ * Converts a string to a single precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
+ *
+ * The decimal separator is assumed to be a dot character.
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose a different format, use cx_strtof_lc().
+ *
+ * @param str the string to convert
+ * @param output a pointer to the float variable where the result shall be stored
+ * @param decsep the decimal separator
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtof_lc(cxstring str, float *output, char decsep, const char *groupsep);
+
+/**
+ * Converts a string to a double precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
+ *
+ * The decimal separator is assumed to be a dot character.
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose a different format, use cx_strtof_lc().
+ *
+ * @param str the string to convert
+ * @param output a pointer to the float variable where the result shall be stored
+ * @param decsep the decimal separator
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2)
+int cx_strtod_lc(cxstring str, double *output, char decsep, const char *groupsep);
+
+#ifndef CX_STR_IMPLEMENTATION
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtos_lc(str, output, base, groupsep) cx_strtos_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoi_lc(str, output, base, groupsep) cx_strtoi_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtol_lc(str, output, base, groupsep) cx_strtol_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoll_lc(str, output, base, groupsep) cx_strtoll_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoi8_lc(str, output, base, groupsep) cx_strtoi8_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoi16_lc(str, output, base, groupsep) cx_strtoi16_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoi32_lc(str, output, base, groupsep) cx_strtoi32_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoi64_lc(str, output, base, groupsep) cx_strtoi64_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoz_lc(str, output, base, groupsep) cx_strtoz_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtous_lc(str, output, base, groupsep) cx_strtous_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtou_lc(str, output, base, groupsep) cx_strtou_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoul_lc(str, output, base, groupsep) cx_strtoul_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtoull_lc(str, output, base, groupsep) cx_strtoull_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtou8_lc(str, output, base, groupsep) cx_strtou8_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtou16_lc(str, output, base, groupsep) cx_strtou16_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtou32_lc(str, output, base, groupsep) cx_strtou32_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * @copydoc cx_strtouz_lc()
+ */
+#define cx_strtou64_lc(str, output, base, groupsep) cx_strtou64_lc(cx_strcast(str), output, base, groupsep)
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtouz_lc(str, output, base, groupsep) cx_strtouz_lc(cx_strcast(str), output, base, groupsep)
+
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtos(str, output, base) cx_strtos_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoi(str, output, base) cx_strtoi_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtol(str, output, base) cx_strtol_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoll(str, output, base) cx_strtoll_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoi8(str, output, base) cx_strtoi8_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoi16(str, output, base) cx_strtoi16_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoi32(str, output, base) cx_strtoi32_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoi64(str, output, base) cx_strtoi64_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoz(str, output, base) cx_strtoz_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtous(str, output, base) cx_strtous_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtou(str, output, base) cx_strtou_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoul(str, output, base) cx_strtoul_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtoull(str, output, base) cx_strtoull_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtou8(str, output, base) cx_strtou8_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtou16(str, output, base) cx_strtou16_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtou32(str, output, base) cx_strtou32_lc(str, output, base, ",")
+/**
+ * @copydoc cx_strtouz()
+ */
+#define cx_strtou64(str, output, base) cx_strtou64_lc(str, output, base, ",")
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtouz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtouz(str, output, base) cx_strtouz_lc(str, output, base, ",")
+
+/**
+ * Converts a string to a single precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
+ *
+ * The decimal separator is assumed to be a dot character.
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose a different format, use cx_strtof_lc().
+ *
+ * @param str the string to convert
+ * @param output a pointer to the float variable where the result shall be stored
+ * @param decsep the decimal separator
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtof_lc(str, output, decsep, groupsep) cx_strtof_lc(cx_strcast(str), output, decsep, groupsep)
+/**
+ * Converts a string to a double precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ *
+ * The decimal separator is assumed to be a dot character.
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose a different format, use cx_strtof_lc().
+ *
+ * @param str the string to convert
+ * @param output a pointer to the double variable where the result shall be stored
+ * @param decsep the decimal separator
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtod_lc(str, output, decsep, groupsep) cx_strtod_lc(cx_strcast(str), output, decsep, groupsep)
+
+/**
+ * Converts a string to a single precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
+ *
+ * The decimal separator is assumed to be a dot character.
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose a different format, use cx_strtof_lc().
+ *
+ * @param str the string to convert
+ * @param output a pointer to the float variable where the result shall be stored
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtof(str, output) cx_strtof_lc(str, output, '.', ",")
+/**
+ * Converts a string to a double precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ *
+ * The decimal separator is assumed to be a dot character.
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose a different format, use cx_strtof_lc().
+ *
+ * @param str the string to convert
+ * @param output a pointer to the double variable where the result shall be stored
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtod(str, output) cx_strtod_lc(str, output, '.', ",")
+
+#endif
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/test.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/test.h	Sun Jan 05 22:00:39 2025 +0100
@@ -35,13 +35,13 @@
  *
  * **** IN HEADER FILE: ****
  *
- * <pre>
+ * <code>
  * CX_TEST(function_name);
  * CX_TEST_SUBROUTINE(subroutine_name, paramlist); // optional
- * </pre>
+ * </code>
  *
  * **** IN SOURCE FILE: ****
- * <pre>
+ * <code>
  * CX_TEST_SUBROUTINE(subroutine_name, paramlist) {
  *   // tests with CX_TEST_ASSERT()
  * }
@@ -54,7 +54,7 @@
  *   }
  *   // cleanup of memory here
  * }
- * </pre>
+ * </code>
  * 
  * @attention Do not call own functions within a test, that use
  * CX_TEST_ASSERT() macros and are not defined by using CX_TEST_SUBROUTINE().
@@ -67,7 +67,8 @@
 #ifndef UCX_TEST_H
 #define	UCX_TEST_H
 
-#include <stdlib.h>
+#include "common.h"
+
 #include <stdio.h>
 #include <string.h>
 #include <setjmp.h>
@@ -86,23 +87,10 @@
 #define __FUNCTION__ __func__
 #endif
 
-//
 #if !defined(__clang__) && __GNUC__ > 3
 #pragma GCC diagnostic ignored "-Wclobbered"
 #endif
 
-#ifndef UCX_COMMON_H
-/**
- * Function pointer compatible with fwrite-like functions.
- */
-typedef size_t (*cx_write_func)(
-        const void *,
-        size_t,
-        size_t,
-        void *
-);
-#endif // UCX_COMMON_H
-
 /** Type for the CxTestSuite. */
 typedef struct CxTestSuite CxTestSuite;
 
@@ -148,6 +136,10 @@
  * @param name optional name of the suite
  * @return a new test suite
  */
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
+cx_attr_malloc
 static inline CxTestSuite* cx_test_suite_new(const char *name) {
     CxTestSuite* suite = (CxTestSuite*) malloc(sizeof(CxTestSuite));
     if (suite != NULL) {
@@ -161,10 +153,12 @@
 }
 
 /**
- * Destroys a test suite.
- * @param suite the test suite to destroy
+ * Deallocates a test suite.
+ *
+ * @param suite the test suite to free
  */
 static inline void cx_test_suite_free(CxTestSuite* suite) {
+    if (suite == NULL) return;
     CxTestSet *l = suite->tests;
     while (l != NULL) {
         CxTestSet *e = l;
@@ -179,8 +173,10 @@
  * 
  * @param suite the suite, the test function shall be added to
  * @param test the test function to register
- * @return zero on success or non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  */
+cx_attr_nonnull
 static inline int cx_test_register(CxTestSuite* suite, CxTest test) {
     CxTestSet *t = (CxTestSet*) malloc(sizeof(CxTestSet));
     if (t) {
@@ -205,8 +201,9 @@
  * Runs a test suite and writes the test log to the specified stream.
  * @param suite the test suite to run
  * @param out_target the target buffer or file to write the output to
- * @param out_writer the write function writing to \p out_target
+ * @param out_writer the write function writing to @p out_target
  */
+cx_attr_nonnull
 static inline void cx_test_run(CxTestSuite *suite,
                                void *out_target, cx_write_func out_writer) {
     if (suite->name == NULL) {
@@ -233,14 +230,14 @@
 
 /**
  * Runs a test suite and writes the test log to the specified FILE stream.
- * @param suite the test suite to run
- * @param file the target file to write the output to
+ * @param suite (@c CxTestSuite*) the test suite to run
+ * @param file (@c FILE*) the target file to write the output to
  */
 #define cx_test_run_f(suite, file) cx_test_run(suite, (void*)file, (cx_write_func)fwrite)
 
 /**
  * Runs a test suite and writes the test log to stdout.
- * @param suite the test suite to run
+ * @param suite (@c CxTestSuite*) the test suite to run
  */
 #define cx_test_run_stdout(suite) cx_test_run_f(suite, stdout)
 
@@ -255,6 +252,17 @@
 
 /**
  * Defines the scope of a test.
+ *
+ * @code
+ * CX_TEST(my_test_name) {
+ *     // setup code
+ *     CX_TEST_DO {
+ *         // your tests go here
+ *     }
+ *     // tear down code
+ * }
+ * @endcode
+ *
  * @attention Any CX_TEST_ASSERT() calls must be performed in scope of
  * #CX_TEST_DO.
  */
@@ -272,8 +280,8 @@
  * If the assertion is correct, the test carries on. If the assertion is not
  * correct, the specified message (terminated by a dot and a line break) is
  * written to the test suites output stream.
- * @param condition the condition to check
- * @param message the message that shall be printed out on failure
+ * @param condition (@c bool) the condition to check
+ * @param message (@c char*) the message that shall be printed out on failure
  */
 #define CX_TEST_ASSERTM(condition,message) if (!(condition)) { \
         const char *_assert_msg_ = message; \
@@ -288,7 +296,7 @@
  * If the assertion is correct, the test carries on. If the assertion is not
  * correct, the specified message (terminated by a dot and a line break) is
  * written to the test suites output stream.
- * @param condition the condition to check
+ * @param condition (@c bool) the condition to check
  */
 #define CX_TEST_ASSERT(condition) CX_TEST_ASSERTM(condition, #condition " failed")
 
--- a/ucx/cx/tree.h	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/cx/tree.h	Sun Jan 05 22:00:39 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file tree.h
- * \brief Interface for tree implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file tree.h
+ * @brief Interface for tree implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_TREE_H
@@ -138,7 +138,7 @@
      */
     size_t depth;
     /**
-     * The next element in the queue or \c NULL.
+     * The next element in the queue or @c NULL.
      */
     struct cx_tree_visitor_queue_s *next;
 };
@@ -209,7 +209,7 @@
  * Releases internal memory of the given tree iterator.
  * @param iter the iterator
  */
- __attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxTreeIteratorDispose(CxTreeIterator *iter) {
     free(iter->stack);
     iter->stack = NULL;
@@ -219,7 +219,7 @@
  * Releases internal memory of the given tree visitor.
  * @param visitor the visitor
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxTreeVisitorDispose(CxTreeVisitor *visitor) {
     struct cx_tree_visitor_queue_s *q = visitor->queue_next;
     while (q != NULL) {
@@ -233,7 +233,7 @@
  * Advises the iterator to skip the subtree below the current node and
  * also continues the current loop.
  *
- * @param iterator the iterator
+ * @param iterator (@c CxTreeIterator) the iterator
  */
 #define cxTreeIteratorContinue(iterator) (iterator).skip = true; continue
 
@@ -241,7 +241,7 @@
  * Advises the visitor to skip the subtree below the current node and
  * also continues the current loop.
  *
- * @param visitor the visitor
+ * @param visitor (@c CxTreeVisitor) the visitor
  */
 #define cxTreeVisitorContinue(visitor) cxTreeIteratorContinue(visitor)
 
@@ -249,7 +249,7 @@
  * Links a node to a (new) parent.
  *
  * If the node has already a parent, it is unlinked, first.
- * If the parent has children already, the node is \em appended to the list
+ * If the parent has children already, the node is @em appended to the list
  * of all currently existing children.
  *
  * @param parent the parent node
@@ -258,14 +258,14 @@
  * @param loc_children offset in the node struct for the children linked list
  * @param loc_last_child optional offset in the node struct for the pointer to
  * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev offset in the node struct for the prev pointer
+ * @param loc_prev optional offset in the node struct for the prev pointer
  * @param loc_next offset in the node struct for the next pointer
  * @see cx_tree_unlink()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_tree_link(
-        void *restrict parent,
-        void *restrict node,
+        void *parent,
+        void *node,
         ptrdiff_t loc_parent,
         ptrdiff_t loc_children,
         ptrdiff_t loc_last_child,
@@ -283,11 +283,11 @@
  * @param loc_children offset in the node struct for the children linked list
  * @param loc_last_child optional offset in the node struct for the pointer to
  * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev offset in the node struct for the prev pointer
+ * @param loc_prev optional offset in the node struct for the prev pointer
  * @param loc_next offset in the node struct for the next pointer
  * @see cx_tree_link()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_tree_unlink(
         void *node,
         ptrdiff_t loc_parent,
@@ -298,14 +298,21 @@
 );
 
 /**
+ * Macro that can be used instead of the magic value for infinite search depth.
+ */
+#define CX_TREE_SEARCH_INFINITE_DEPTH 0
+
+/**
  * Function pointer for a search function.
  *
- * A function of this kind shall check if the specified \p node
- * contains the given \p data or if one of the children might contain
+ * A function of this kind shall check if the specified @p node
+ * contains the given @p data or if one of the children might contain
  * the data.
  *
  * The function should use the returned integer to indicate how close the
  * match is, where a negative number means that it does not match at all.
+ * Zero means exact match and a positive number is an implementation defined
+ * measure for the distance to an exact match.
  *
  * For example if a tree stores file path information, a node that is
  * describing a parent directory of a filename that is searched, shall
@@ -321,18 +328,21 @@
  * positive if one of the children might contain the data,
  * negative if neither the node, nor the children contains the data
  */
+cx_attr_nonnull
 typedef int (*cx_tree_search_data_func)(const void *node, const void *data);
 
 
 /**
  * Function pointer for a search function.
  *
- * A function of this kind shall check if the specified \p node
- * contains the same \p data as \p new_node or if one of the children might
+ * A function of this kind shall check if the specified @p node
+ * contains the same @p data as @p new_node or if one of the children might
  * contain the data.
  *
  * The function should use the returned integer to indicate how close the
  * match is, where a negative number means that it does not match at all.
+ * Zero means exact match and a positive number is an implementation defined
+ * measure for the distance to an exact match.
  *
  * For example if a tree stores file path information, a node that is
  * describing a parent directory of a filename that is searched, shall
@@ -344,10 +354,11 @@
  * @param node the node that is currently investigated
  * @param new_node a new node with the information which is searched
  *
- * @return 0 if \p node contains the same data as \p new_node,
+ * @return 0 if @p node contains the same data as @p new_node,
  * positive if one of the children might contain the data,
  * negative if neither the node, nor the children contains the data
  */
+cx_attr_nonnull
 typedef int (*cx_tree_search_func)(const void *node, const void *new_node);
 
 /**
@@ -359,11 +370,12 @@
  *
  * Depending on the tree structure it is not necessarily guaranteed that the
  * "closest" match is uniquely defined. This function will search for a node
- * with the best match according to the \p sfunc (meaning: the return value of
- * \p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
+ * with the best match according to the @p sfunc (meaning: the return value of
+ * @p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
  * node matching the criteria is returned.
  *
  * @param root the root node
+ * @param depth the maximum depth (zero=indefinite, one=just root)
  * @param data the data to search for
  * @param sfunc the search function
  * @param result where the result shall be stored
@@ -373,9 +385,11 @@
  * could contain the node (but doesn't right now), negative if the tree does not
  * contain any node that might be related to the searched data
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_w(5)
 int cx_tree_search_data(
         const void *root,
+        size_t depth,
         const void *data,
         cx_tree_search_data_func sfunc,
         void **result,
@@ -392,11 +406,12 @@
  *
  * Depending on the tree structure it is not necessarily guaranteed that the
  * "closest" match is uniquely defined. This function will search for a node
- * with the best match according to the \p sfunc (meaning: the return value of
- * \p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
+ * with the best match according to the @p sfunc (meaning: the return value of
+ * @p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
  * node matching the criteria is returned.
  *
  * @param root the root node
+* @param depth the maximum depth (zero=indefinite, one=just root)
  * @param node the node to search for
  * @param sfunc the search function
  * @param result where the result shall be stored
@@ -406,9 +421,11 @@
  * could contain the node (but doesn't right now), negative if the tree does not
  * contain any node that might be related to the searched data
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_w(5)
 int cx_tree_search(
         const void *root,
+        size_t depth,
         const void *node,
         cx_tree_search_func sfunc,
         void **result,
@@ -436,6 +453,7 @@
  * @return the new tree iterator
  * @see cxTreeIteratorDispose()
  */
+cx_attr_nodiscard
 CxTreeIterator cx_tree_iterator(
         void *root,
         bool visit_on_exit,
@@ -461,6 +479,7 @@
  * @return the new tree visitor
  * @see cxTreeVisitorDispose()
  */
+cx_attr_nodiscard
 CxTreeVisitor cx_tree_visitor(
         void *root,
         ptrdiff_t loc_children,
@@ -472,11 +491,12 @@
  * The first argument points to the data the node shall contain and
  * the second argument may be used for additional data (e.g. an allocator).
  * Functions of this type shall either return a new pointer to a newly
- * created node or \c NULL when allocation fails.
+ * created node or @c NULL when allocation fails.
  *
- * \note the function may leave the node pointers in the struct uninitialized.
+ * @note the function may leave the node pointers in the struct uninitialized.
  * The caller is responsible to set them according to the intended use case.
  */
+cx_attr_nonnull_arg(1)
 typedef void *(*cx_tree_node_create_func)(const void *, void *);
 
 /**
@@ -493,11 +513,11 @@
  * Once an element cannot be added to the tree, this function returns, leaving
  * the iterator in a valid state pointing to the element that could not be
  * added.
- * Also, the pointer of the created node will be stored to \p failed.
+ * Also, the pointer of the created node will be stored to @p failed.
  * The integer returned by this function denotes the number of elements obtained
- * from the \p iter that have been successfully processed.
- * When all elements could be processed, a \c NULL pointer will be written to
- * \p failed.
+ * from the @p iter that have been successfully processed.
+ * When all elements could be processed, a @c NULL pointer will be written to
+ * @p failed.
  *
  * The advantage of this function compared to multiple invocations of
  * #cx_tree_add() is that the search for the insert locations is not always
@@ -520,12 +540,13 @@
  * @param loc_children offset in the node struct for the children linked list
  * @param loc_last_child optional offset in the node struct for the pointer to
  * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev offset in the node struct for the prev pointer
+ * @param loc_prev optional offset in the node struct for the prev pointer
  * @param loc_next offset in the node struct for the next pointer
  * @return the number of nodes created and added
  * @see cx_tree_add()
  */
-__attribute__((__nonnull__(1, 3, 4, 6, 7)))
+cx_attr_nonnull_arg(1, 3, 4, 6, 7)
+cx_attr_access_w(6)
 size_t cx_tree_add_iter(
         struct cx_iterator_base_s *iter,
         size_t num,
@@ -545,11 +566,11 @@
  * Adds multiple elements efficiently to a tree.
  *
  * Once an element cannot be added to the tree, this function returns, storing
- * the pointer of the created node to \p failed.
+ * the pointer of the created node to @p failed.
  * The integer returned by this function denotes the number of elements from
- * the \p src array that have been successfully processed.
- * When all elements could be processed, a \c NULL pointer will be written to
- * \p failed.
+ * the @p src array that have been successfully processed.
+ * When all elements could be processed, a @c NULL pointer will be written to
+ * @p failed.
  *
  * The advantage of this function compared to multiple invocations of
  * #cx_tree_add() is that the search for the insert locations is not always
@@ -562,8 +583,8 @@
  * Refer to the documentation of #cx_tree_add() for more details.
  *
  * @param src a pointer to the source data array
- * @param num the number of elements in the \p src array
- * @param elem_size the size of each element in the \p src array
+ * @param num the number of elements in the @p src array
+ * @param elem_size the size of each element in the @p src array
  * @param sfunc a search function
  * @param cfunc a node creation function
  * @param cdata optional additional data
@@ -573,12 +594,13 @@
  * @param loc_children offset in the node struct for the children linked list
  * @param loc_last_child optional offset in the node struct for the pointer to
  * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev offset in the node struct for the prev pointer
+ * @param loc_prev optional offset in the node struct for the prev pointer
  * @param loc_next offset in the node struct for the next pointer
  * @return the number of array elements successfully processed
  * @see cx_tree_add()
  */
-__attribute__((__nonnull__(1, 4, 5, 7, 8)))
+cx_attr_nonnull_arg(1, 4, 5, 7, 8)
+cx_attr_access_w(7)
 size_t cx_tree_add_array(
         const void *src,
         size_t num,
@@ -599,28 +621,28 @@
  * Adds data to a tree.
  *
  * An adequate location where to add the new tree node is searched with the
- * specified \p sfunc.
+ * specified @p sfunc.
  *
- * When a location is found, the \p cfunc will be invoked with \p cdata.
+ * When a location is found, the @p cfunc will be invoked with @p cdata.
  *
- * The node returned by \p cfunc will be linked into the tree.
- * When \p sfunc returned a positive integer, the new node will be linked as a
+ * The node returned by @p cfunc will be linked into the tree.
+ * When @p sfunc returned a positive integer, the new node will be linked as a
  * child. The other children (now siblings of the new node) are then checked
- * with \p sfunc, whether they could be children of the new node and re-linked
+ * with @p sfunc, whether they could be children of the new node and re-linked
  * accordingly.
  *
- * When \p sfunc returned zero and the found node has a parent, the new
+ * When @p sfunc returned zero and the found node has a parent, the new
  * node will be added as sibling - otherwise, the new node will be added
  * as a child.
  *
- * When \p sfunc returned a negative value, the new node will not be added to
+ * When @p sfunc returned a negative value, the new node will not be added to
  * the tree and this function returns a non-zero value.
- * The caller should check if \p cnode contains a node pointer and deal with the
+ * The caller should check if @p cnode contains a node pointer and deal with the
  * node that could not be added.
  *
- * This function also returns a non-zero value when \p cfunc tries to allocate
- * a new node but fails to do so. In that case, the pointer stored to \p cnode
- * will be \c NULL.
+ * This function also returns a non-zero value when @p cfunc tries to allocate
+ * a new node but fails to do so. In that case, the pointer stored to @p cnode
+ * will be @c NULL.
  *
  * Multiple elements can be added more efficiently with
  * #cx_tree_add_array() or #cx_tree_add_iter().
@@ -635,12 +657,13 @@
  * @param loc_children offset in the node struct for the children linked list
  * @param loc_last_child optional offset in the node struct for the pointer to
  * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev offset in the node struct for the prev pointer
+ * @param loc_prev optional offset in the node struct for the prev pointer
  * @param loc_next offset in the node struct for the next pointer
  * @return zero when a new node was created and added to the tree,
  * non-zero otherwise
  */
-__attribute__((__nonnull__(1, 2, 3, 5, 6)))
+cx_attr_nonnull_arg(1, 2, 3, 5, 6)
+cx_attr_access_w(5)
 int cx_tree_add(
         const void *src,
         cx_tree_search_func sfunc,
@@ -704,7 +727,7 @@
     /**
      * A pointer to the root node.
      *
-     * Will be \c NULL when \c size is 0.
+     * Will be @c NULL when @c size is 0.
      */
     void *root;
 
@@ -778,6 +801,10 @@
 /**
  * Macro to roll out the #cx_tree_node_base_s structure with a custom
  * node type.
+ *
+ * Must be used as first member in your custom tree struct.
+ *
+ * @param type the data type for the nodes
  */
 #define CX_TREE_NODE_BASE(type) \
     type *parent; \
@@ -788,6 +815,11 @@
 
 /**
  * Macro for specifying the layout of a base node tree.
+ *
+ * When your tree uses #CX_TREE_NODE_BASE, you can use this
+ * macro in all tree functions that expect the layout parameters
+ * @c loc_parent, @c loc_children, @c loc_last_child, @c loc_prev,
+ * and @c loc_next.
  */
 #define cx_tree_node_base_layout \
     offsetof(struct cx_tree_node_base_s, parent),\
@@ -797,31 +829,13 @@
     offsetof(struct cx_tree_node_base_s, next)
 
 /**
- * Macro for obtaining the node pointer layout for a specific tree.
- */
-#define cx_tree_node_layout(tree) \
-    (tree)->loc_parent,\
-    (tree)->loc_children,\
-    (tree)->loc_last_child,\
-    (tree)->loc_prev,  \
-    (tree)->loc_next
-
-/**
  * The class definition for arbitrary trees.
  */
 struct cx_tree_class_s {
     /**
-     * Destructor function.
-     *
-     * Implementations SHALL invoke the node destructor functions if provided
-     * and SHALL deallocate the tree memory.
-     */
-    void (*destructor)(struct cx_tree_s *);
-
-    /**
      * Member function for inserting a single element.
      *
-     * Implementations SHALL NOT simply invoke \p insert_many as this comes
+     * Implementations SHALL NOT simply invoke @p insert_many as this comes
      * with too much overhead.
      */
     int (*insert_element)(
@@ -847,21 +861,9 @@
     void *(*find)(
             struct cx_tree_s *tree,
             const void *subtree,
-            const void *data
+            const void *data,
+            size_t depth
     );
-
-    /**
-     * Member function for creating an iterator for the tree.
-     */
-    CxTreeIterator (*iterator)(
-            struct cx_tree_s *tree,
-            bool visit_on_exit
-    );
-
-    /**
-     * Member function for creating a visitor for the tree.
-     */
-    CxTreeVisitor (*visitor)(struct cx_tree_s *tree);
 };
 
 /**
@@ -869,16 +871,80 @@
  */
 typedef struct cx_tree_s CxTree;
 
+
+/**
+ * Destroys a node and it's subtree.
+ *
+ * It is guaranteed that the simple destructor is invoked before
+ * the advanced destructor, starting with the leaf nodes of the subtree.
+ *
+ * When this function is invoked on the root node of the tree, it destroys the
+ * tree contents, but - in contrast to #cxTreeFree() - not the tree
+ * structure, leaving an empty tree behind.
+ *
+ * @note The destructor function, if any, will @em not be invoked. That means
+ * you will need to free the removed subtree by yourself, eventually.
+ *
+ * @attention This function will not free the memory of the nodes with the
+ * tree's allocator, because that is usually done by the advanced destructor
+ * and would therefore result in a double-free.
+ *
+ * @param tree the tree
+ * @param node the node to remove
+ * @see cxTreeFree()
+ */
+cx_attr_nonnull
+void cxTreeDestroySubtree(CxTree *tree, void *node);
+
+
+/**
+ * Destroys the tree contents.
+ *
+ * It is guaranteed that the simple destructor is invoked before
+ * the advanced destructor, starting with the leaf nodes of the subtree.
+ *
+ * This is a convenience macro for invoking #cxTreeDestroySubtree() on the
+ * root node of the tree.
+ *
+ * @attention Be careful when calling this function when no destructor function
+ * is registered that actually frees the memory of nodes. In that case you will
+ * need a reference to the (former) root node of the tree somewhere or
+ * otherwise you will be leaking memory.
+ *
+ * @param tree the tree
+ * @see cxTreeDestroySubtree()
+ */
+#define cxTreeClear(tree) cxTreeDestroySubtree(tree, tree->root)
+
+/**
+ * Deallocates the tree structure.
+ *
+ * The destructor functions are invoked for each node, starting with the leaf
+ * nodes.
+ * It is guaranteed that for each node the simple destructor is invoked before
+ * the advanced destructor.
+ *
+ * @attention This function will only invoke the destructor functions
+ * on the nodes.
+ * It will NOT additionally free the nodes with the tree's allocator, because
+ * that would cause a double-free in most scenarios where the advanced
+ * destructor is already freeing the memory.
+ *
+ * @param tree the tree to free
+ */
+void cxTreeFree(CxTree *tree);
+
 /**
  * Creates a new tree structure based on the specified layout.
  *
- * The specified \p allocator will be used for creating the tree struct
- * and SHALL be used by \p create_func to allocate memory for the nodes.
+ * The specified @p allocator will be used for creating the tree struct
+ * and SHALL be used by @p create_func to allocate memory for the nodes.
  *
- * \note This function will also register an advanced destructor which
+ * @note This function will also register an advanced destructor which
  * will free the nodes with the allocator's free() method.
  *
  * @param allocator the allocator that shall be used
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param create_func a function that creates new nodes
  * @param search_func a function that compares two nodes
  * @param search_data_func a function that compares a node with data
@@ -886,13 +952,16 @@
  * @param loc_children offset in the node struct for the children linked list
  * @param loc_last_child optional offset in the node struct for the pointer to
  * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev offset in the node struct for the prev pointer
+ * @param loc_prev optional offset in the node struct for the prev pointer
  * @param loc_next offset in the node struct for the next pointer
  * @return the new tree
  * @see cxTreeCreateSimple()
  * @see cxTreeCreateWrapped()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull_arg(2, 3, 4)
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxTreeFree, 1)
 CxTree *cxTreeCreate(
         const CxAllocator *allocator,
         cx_tree_node_create_func create_func,
@@ -908,57 +977,51 @@
 /**
  * Creates a new tree structure based on a default layout.
  *
- * Nodes created by \p create_func MUST contain #cx_tree_node_base_s as first
+ * Nodes created by @p create_func MUST contain #cx_tree_node_base_s as first
  * member (or at least respect the default offsets specified in the tree
  * struct) and they MUST be allocated with the specified allocator.
  *
- * \note This function will also register an advanced destructor which
+ * @note This function will also register an advanced destructor which
  * will free the nodes with the allocator's free() method.
  *
- * @param allocator the allocator that shall be used
- * @param create_func a function that creates new nodes
- * @param search_func a function that compares two nodes
- * @param search_data_func a function that compares a node with data
- * @return the new tree
+ * @param allocator (@c CxAllocator*) the allocator that shall be used
+ * @param create_func (@c cx_tree_node_create_func) a function that creates new nodes
+ * @param search_func (@c cx_tree_search_func) a function that compares two nodes
+ * @param search_data_func (@c cx_tree_search_data_func)  a function that compares a node with data
+ * @return (@c CxTree*) the new tree
  * @see cxTreeCreate()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline CxTree *cxTreeCreateSimple(
-        const CxAllocator *allocator,
-        cx_tree_node_create_func create_func,
-        cx_tree_search_func search_func,
-        cx_tree_search_data_func search_data_func
-) {
-    return cxTreeCreate(
-            allocator,
-            create_func,
-            search_func,
-            search_data_func,
-            cx_tree_node_base_layout
-    );
-}
+#define cxTreeCreateSimple(\
+    allocator, create_func, search_func, search_data_func \
+) cxTreeCreate(allocator, create_func, search_func, search_data_func, \
+cx_tree_node_base_layout)
 
 /**
  * Creates a new tree structure based on an existing tree.
  *
- * The specified \p allocator will be used for creating the tree struct.
+ * The specified @p allocator will be used for creating the tree struct.
  *
- * \attention This function will create an incompletely defined tree structure
+ * @attention This function will create an incompletely defined tree structure
  * where neither the create function, the search function, nor a destructor
  * will be set. If you wish to use any of this functionality for the wrapped
  * tree, you need to specify those functions afterwards.
  *
+ * @param allocator the allocator that was used for nodes of the wrapped tree
+ * (if @c NULL, a default stdlib allocator is assumed)
  * @param root the root node of the tree that shall be wrapped
  * @param loc_parent offset in the node struct for the parent pointer
  * @param loc_children offset in the node struct for the children linked list
  * @param loc_last_child optional offset in the node struct for the pointer to
  * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev offset in the node struct for the prev pointer
+ * @param loc_prev optional offset in the node struct for the prev pointer
  * @param loc_next offset in the node struct for the next pointer
  * @return the new tree
  * @see cxTreeCreate()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull_arg(2)
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxTreeFree, 1)
 CxTree *cxTreeCreateWrapped(
         const CxAllocator *allocator,
         void *root,
@@ -970,32 +1033,18 @@
 );
 
 /**
- * Destroys the tree structure.
- *
- * \attention This function will only invoke the destructor functions
- * on the nodes, if specified.
- * It will NOT additionally free the nodes with the tree's allocator, because
- * that would cause a double-free in most scenarios.
- *
- * @param tree the tree to destroy
- */
-__attribute__((__nonnull__))
-static inline void cxTreeDestroy(CxTree *tree) {
-    tree->cl->destructor(tree);
-}
-
-/**
  * Inserts data into the tree.
  *
- * \remark For this function to work, the tree needs specified search and
+ * @remark For this function to work, the tree needs specified search and
  * create functions, which might not be available for wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
  * @param tree the tree
  * @param data the data to insert
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxTreeInsert(
         CxTree *tree,
         const void *data
@@ -1006,7 +1055,7 @@
 /**
  * Inserts elements provided by an iterator efficiently into the tree.
  *
- * \remark For this function to work, the tree needs specified search and
+ * @remark For this function to work, the tree needs specified search and
  * create functions, which might not be available for wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
@@ -1015,7 +1064,7 @@
  * @param n the maximum number of elements to insert
  * @return the number of elements that could be successfully inserted
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxTreeInsertIter(
         CxTree *tree,
         struct cx_iterator_base_s *iter,
@@ -1027,7 +1076,7 @@
 /**
  * Inserts an array of data efficiently into the tree.
  *
- * \remark For this function to work, the tree needs specified search and
+ * @remark For this function to work, the tree needs specified search and
  * create functions, which might not be available for wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
@@ -1037,7 +1086,7 @@
  * @param n the number of elements in the array
  * @return the number of elements that could be successfully inserted
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxTreeInsertArray(
         CxTree *tree,
         const void *data,
@@ -1053,44 +1102,51 @@
 /**
  * Searches the data in the specified tree.
  *
- * \remark For this function to work, the tree needs a specified \c search_data
+ * @remark For this function to work, the tree needs a specified @c search_data
  * function, which might not be available wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
  * @param tree the tree
  * @param data the data to search for
- * @return the first matching node, or \c NULL when the data cannot be found
+ * @return the first matching node, or @c NULL when the data cannot be found
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxTreeFind(
         CxTree *tree,
         const void *data
 ) {
-    return tree->cl->find(tree, tree->root, data);
+    return tree->cl->find(tree, tree->root, data, 0);
 }
 
 /**
  * Searches the data in the specified subtree.
  *
- * \note When \p subtree_root is not part of the \p tree, the behavior is
+ * When @p max_depth is zero, the depth is not limited.
+ * The @p subtree_root itself is on depth 1 and its children have depth 2.
+ *
+ * @note When @p subtree_root is not part of the @p tree, the behavior is
  * undefined.
  *
- * \remark For this function to work, the tree needs a specified \c search_data
+ * @remark For this function to work, the tree needs a specified @c search_data
  * function, which might not be the case for wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
  * @param tree the tree
  * @param data the data to search for
  * @param subtree_root the node where to start
- * @return the first matching node, or \c NULL when the data cannot be found
+ * @param max_depth the maximum search depth
+ * @return the first matching node, or @c NULL when the data cannot be found
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxTreeFindInSubtree(
         CxTree *tree,
         const void *data,
-        void *subtree_root
+        void *subtree_root,
+        size_t max_depth
 ) {
-    return tree->cl->find(tree, subtree_root, data);
+    return tree->cl->find(tree, subtree_root, data, max_depth);
 }
 
 /**
@@ -1100,7 +1156,8 @@
  * @param subtree_root the root node of the subtree
  * @return the number of nodes in the specified subtree
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
 
 /**
@@ -1108,9 +1165,10 @@
  *
  * @param tree the tree
  * @param subtree_root the root node of the subtree
- * @return the tree depth including the \p subtree_root
+ * @return the tree depth including the @p subtree_root
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
 
 /**
@@ -1119,24 +1177,69 @@
  * @param tree the tree
  * @return the tree depth, counting the root as one
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 size_t cxTreeDepth(CxTree *tree);
 
 /**
+ * Creates a depth-first iterator for the specified tree starting in @p node.
+ *
+ * If the node is not part of the tree, the behavior is undefined.
+ *
+ * @param tree the tree to iterate
+ * @param node the node where to start
+ * @param visit_on_exit true, if the iterator shall visit a node again when
+ * leaving the subtree
+ * @return a tree iterator (depth-first)
+ * @see cxTreeVisit()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline CxTreeIterator cxTreeIterateSubtree(
+        CxTree *tree,
+        void *node,
+        bool visit_on_exit
+) {
+    return cx_tree_iterator(
+            node, visit_on_exit,
+            tree->loc_children, tree->loc_next
+    );
+}
+
+/**
+ * Creates a breadth-first iterator for the specified tree starting in @p node.
+ *
+ * If the node is not part of the tree, the behavior is undefined.
+ *
+ * @param tree the tree to iterate
+ * @param node the node where to start
+ * @return a tree visitor (a.k.a. breadth-first iterator)
+ * @see cxTreeIterate()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline CxTreeVisitor cxTreeVisitSubtree(CxTree *tree, void *node) {
+    return cx_tree_visitor(
+            node, tree->loc_children, tree->loc_next
+    );
+}
+
+/**
  * Creates a depth-first iterator for the specified tree.
  *
  * @param tree the tree to iterate
  * @param visit_on_exit true, if the iterator shall visit a node again when
- * leaving the sub-tree
+ * leaving the subtree
  * @return a tree iterator (depth-first)
- * @see cxTreeVisitor()
+ * @see cxTreeVisit()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline CxTreeIterator cxTreeIterator(
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline CxTreeIterator cxTreeIterate(
         CxTree *tree,
         bool visit_on_exit
 ) {
-    return tree->cl->iterator(tree, visit_on_exit);
+    return cxTreeIterateSubtree(tree, tree->root, visit_on_exit);
 }
 
 /**
@@ -1144,32 +1247,53 @@
  *
  * @param tree the tree to iterate
  * @return a tree visitor (a.k.a. breadth-first iterator)
- * @see cxTreeIterator()
+ * @see cxTreeIterate()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline CxTreeVisitor cxTreeVisitor(CxTree *tree) {
-    return tree->cl->visitor(tree);
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline CxTreeVisitor cxTreeVisit(CxTree *tree) {
+    return cxTreeVisitSubtree(tree, tree->root);
 }
 
 /**
+ * Sets the (new) parent of the specified child.
+ *
+ * If the @p child is not already member of the tree, this function behaves
+ * as #cxTreeAddChildNode().
+ *
+ * @param tree the tree
+ * @param parent the (new) parent of the child
+ * @param child the node to add
+ * @see cxTreeAddChildNode()
+ */
+cx_attr_nonnull
+void cxTreeSetParent(
+        CxTree *tree,
+        void *parent,
+        void *child
+);
+
+/**
  * Adds a new node to the tree.
  *
- * \attention The node may be externally created, but MUST obey the same rules
+ * If the @p child is already member of the tree, the behavior is undefined.
+ * Use #cxTreeSetParent() if you want to move a subtree to another location.
+ *
+ * @attention The node may be externally created, but MUST obey the same rules
  * as if it was created by the tree itself with #cxTreeAddChild() (e.g. use
  * the same allocator).
  *
  * @param tree the tree
  * @param parent the parent of the node to add
  * @param child the node to add
+ * @see cxTreeSetParent()
  */
-__attribute__((__nonnull__))
-static inline void cxTreeAddChildNode(
+cx_attr_nonnull
+void cxTreeAddChildNode(
         CxTree *tree,
         void *parent,
-        void *child) {
-    cx_tree_link(parent, child, cx_tree_node_layout(tree));
-    tree->size++;
-}
+        void *child
+);
 
 /**
  * Creates a new node and adds it to the tree.
@@ -1188,7 +1312,7 @@
  * @return zero when the new node was created, non-zero on allocation failure
  * @see cxTreeInsert()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxTreeAddChild(
         CxTree *tree,
         void *parent,
@@ -1199,13 +1323,15 @@
  * A function that is invoked when a node needs to be re-linked to a new parent.
  *
  * When a node is re-linked, sometimes the contents need to be updated.
- * This callback is invoked by #cxTreeRemove() so that those updates can be
- * applied when re-linking the children of the removed node.
+ * This callback is invoked by #cxTreeRemoveNode() and #cxTreeDestroyNode()
+ * so that those updates can be applied when re-linking the children of the
+ * removed node.
  *
  * @param node the affected node
  * @param old_parent the old parent of the node
  * @param new_parent the new parent of the node
  */
+cx_attr_nonnull
 typedef void (*cx_tree_relink_func)(
         void *node,
         const void *old_parent,
@@ -1217,17 +1343,17 @@
  *
  * If the node is not part of the tree, the behavior is undefined.
  *
- * \note The destructor function, if any, will \em not be invoked. That means
+ * @note The destructor function, if any, will @em not be invoked. That means
  * you will need to free the removed node by yourself, eventually.
  *
  * @param tree the tree
  * @param node the node to remove (must not be the root node)
  * @param relink_func optional callback to update the content of each re-linked
  * node
- * @return zero on success, non-zero if \p node is the root node of the tree
+ * @return zero on success, non-zero if @p node is the root node of the tree
  */
-__attribute__((__nonnull__(1,2)))
-int cxTreeRemove(
+cx_attr_nonnull_arg(1, 2)
+int cxTreeRemoveNode(
         CxTree *tree,
         void *node,
         cx_tree_relink_func relink_func
@@ -1238,15 +1364,40 @@
  *
  * If the node is not part of the tree, the behavior is undefined.
  *
- * \note The destructor function, if any, will \em not be invoked. That means
+ * @note The destructor function, if any, will @em not be invoked. That means
  * you will need to free the removed subtree by yourself, eventually.
  *
  * @param tree the tree
  * @param node the node to remove
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxTreeRemoveSubtree(CxTree *tree, void *node);
 
+/**
+ * Destroys a node and re-links its children to its former parent.
+ *
+ * If the node is not part of the tree, the behavior is undefined.
+ *
+ * It is guaranteed that the simple destructor is invoked before
+ * the advanced destructor.
+ *
+ * @attention This function will not free the memory of the node with the
+ * tree's allocator, because that is usually done by the advanced destructor
+ * and would therefore result in a double-free.
+ *
+ * @param tree the tree
+ * @param node the node to destroy (must not be the root node)
+ * @param relink_func optional callback to update the content of each re-linked
+ * node
+ * @return zero on success, non-zero if @p node is the root node of the tree
+ */
+cx_attr_nonnull_arg(1, 2)
+int cxTreeDestroyNode(
+        CxTree *tree,
+        void *node,
+        cx_tree_relink_func relink_func
+);
+
 #ifdef __cplusplus
 } // extern "C"
 #endif
--- a/ucx/hash_key.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/hash_key.c	Sun Jan 05 22:00:39 2025 +0100
@@ -40,7 +40,7 @@
 
     unsigned m = 0x5bd1e995;
     unsigned r = 24;
-    unsigned h = 25 ^ len;
+    unsigned h = 25 ^ (unsigned) len;
     unsigned i = 0;
     while (len >= 4) {
         unsigned k = data[i + 0] & 0xFF;
--- a/ucx/hash_map.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/hash_map.c	Sun Jan 05 22:00:39 2025 +0100
@@ -27,10 +27,10 @@
  */
 
 #include "cx/hash_map.h"
-#include "cx/utils.h"
 
 #include <string.h>
 #include <assert.h>
+#include <errno.h>
 
 struct cx_hash_map_element_s {
     /** A pointer to the next element in the current bucket. */
@@ -45,7 +45,7 @@
 
 static void cx_hash_map_clear(struct cx_map_s *map) {
     struct cx_hash_map_s *hash_map = (struct cx_hash_map_s *) map;
-    cx_for_n(i, hash_map->bucket_count) {
+    for (size_t i = 0; i < hash_map->bucket_count; i++) {
         struct cx_hash_map_element_s *elem = hash_map->buckets[i];
         if (elem != NULL) {
             do {
@@ -115,9 +115,7 @@
                 allocator,
                 sizeof(struct cx_hash_map_element_s) + map->collection.elem_size
         );
-        if (e == NULL) {
-            return -1;
-        }
+        if (e == NULL) return -1;
 
         // write the value
         if (map->collection.store_pointer) {
@@ -128,9 +126,7 @@
 
         // copy the key
         void *kd = cxMalloc(allocator, key.len);
-        if (kd == NULL) {
-            return -1;
-        }
+        if (kd == NULL) return -1;
         memcpy(kd, key.data, key.len);
         e->key.data = kd;
         e->key.len = key.len;
@@ -173,17 +169,28 @@
 /**
  * Helper function to avoid code duplication.
  *
+ * If @p remove is true, and @p targetbuf is @c NULL, the element
+ * will be destroyed when found.
+ *
+ * If @p remove is true, and @p targetbuf is set, the element will
+ * be copied to that buffer and no destructor function is called.
+ *
+ * If @p remove is false, @p targetbuf must not be non-null and
+ * either the pointer, when the map is storing pointers, is copied
+ * to the target buffer, or a pointer to the stored object will
+ * be copied to the target buffer.
+ *
  * @param map the map
  * @param key the key to look up
+ * @param targetbuf see description
  * @param remove flag indicating whether the looked up entry shall be removed
- * @param destroy flag indicating whether the destructor shall be invoked
- * @return a pointer to the value corresponding to the key or \c NULL
+ * @return zero, if the key was found, non-zero otherwise
  */
-static void *cx_hash_map_get_remove(
+static int cx_hash_map_get_remove(
         CxMap *map,
         CxHashKey key,
-        bool remove,
-        bool destroy
+        void *targetbuf,
+        bool remove
 ) {
     struct cx_hash_map_s *hash_map = (struct cx_hash_map_s *) map;
 
@@ -199,27 +206,31 @@
     while (elm && elm->key.hash <= hash) {
         if (elm->key.hash == hash && elm->key.len == key.len) {
             if (memcmp(elm->key.data, key.data, key.len) == 0) {
-                void *data = NULL;
-                if (destroy) {
-                    cx_invoke_destructor(map, elm->data);
+                if (remove) {
+                    if (targetbuf == NULL) {
+                        cx_invoke_destructor(map, elm->data);
+                    } else {
+                        memcpy(targetbuf, elm->data, map->collection.elem_size);
+                    }
+                    cx_hash_map_unlink(hash_map, slot, prev, elm);
                 } else {
+                    assert(targetbuf != NULL);
+                    void *data = NULL;
                     if (map->collection.store_pointer) {
                         data = *(void **) elm->data;
                     } else {
                         data = elm->data;
                     }
+                    memcpy(targetbuf, &data, sizeof(void *));
                 }
-                if (remove) {
-                    cx_hash_map_unlink(hash_map, slot, prev, elm);
-                }
-                return data;
+                return 0;
             }
         }
         prev = elm;
         elm = prev->next;
     }
 
-    return NULL;
+    return 1;
 }
 
 static void *cx_hash_map_get(
@@ -227,15 +238,17 @@
         CxHashKey key
 ) {
     // we can safely cast, because we know the map stays untouched
-    return cx_hash_map_get_remove((CxMap *) map, key, false, false);
+    void *ptr = NULL;
+    int found = cx_hash_map_get_remove((CxMap *) map, key, &ptr, false);
+    return found == 0 ? ptr : NULL;
 }
 
-static void *cx_hash_map_remove(
+static int cx_hash_map_remove(
         CxMap *map,
         CxHashKey key,
-        bool destroy
+        void *targetbuf
 ) {
-    return cx_hash_map_get_remove(map, key, true, destroy);
+    return cx_hash_map_get_remove(map, key, targetbuf, true);
 }
 
 static void *cx_hash_map_iter_current_entry(const void *it) {
@@ -346,7 +359,7 @@
             iter.base.current = cx_hash_map_iter_current_value;
             break;
         default:
-            assert(false);
+            assert(false); // LCOV_EXCL_LINE
     }
 
     iter.base.valid = cx_hash_map_iter_valid;
@@ -393,6 +406,10 @@
         size_t itemsize,
         size_t buckets
 ) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
+    }
+
     if (buckets == 0) {
         // implementation defined default
         buckets = 16;
@@ -406,10 +423,10 @@
     map->bucket_count = buckets;
     map->buckets = cxCalloc(allocator, buckets,
                             sizeof(struct cx_hash_map_element_s *));
-    if (map->buckets == NULL) {
+    if (map->buckets == NULL) { // LCOV_EXCL_START
         cxFree(allocator, map);
         return NULL;
-    }
+    } // LCOV_EXCL_STOP
 
     // initialize base members
     map->base.cl = &cx_hash_map_class;
@@ -431,17 +448,19 @@
     if (map->collection.size > ((hash_map->bucket_count * 3) >> 2)) {
 
         size_t new_bucket_count = (map->collection.size * 5) >> 1;
+        if (new_bucket_count < hash_map->bucket_count) {
+            errno = EOVERFLOW;
+            return 1;
+        }
         struct cx_hash_map_element_s **new_buckets = cxCalloc(
                 map->collection.allocator,
                 new_bucket_count, sizeof(struct cx_hash_map_element_s *)
         );
 
-        if (new_buckets == NULL) {
-            return 1;
-        }
+        if (new_buckets == NULL) return 1;
 
         // iterate through the elements and assign them to their new slots
-        cx_for_n(slot, hash_map->bucket_count) {
+        for (size_t slot = 0; slot < hash_map->bucket_count; slot++) {
             struct cx_hash_map_element_s *elm = hash_map->buckets[slot];
             while (elm != NULL) {
                 struct cx_hash_map_element_s *next = elm->next;
--- a/ucx/iterator.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/iterator.c	Sun Jan 05 22:00:39 2025 +0100
@@ -40,6 +40,11 @@
     return iter->elem_handle;
 }
 
+static void *cx_iter_current_ptr(const void *it) {
+    const struct cx_iterator_s *iter = it;
+    return *(void**)iter->elem_handle;
+}
+
 static void cx_iter_next_fast(void *it) {
     struct cx_iterator_s *iter = it;
     if (iter->base.remove) {
@@ -110,3 +115,22 @@
     iter.base.mutating = false;
     return iter;
 }
+
+CxIterator cxMutIteratorPtr(
+        void *array,
+        size_t elem_count,
+        bool remove_keeps_order
+) {
+    CxIterator iter = cxMutIterator(array, sizeof(void*), elem_count, remove_keeps_order);
+    iter.base.current = cx_iter_current_ptr;
+    return iter;
+}
+
+CxIterator cxIteratorPtr(
+        const void *array,
+        size_t elem_count
+) {
+    CxIterator iter = cxMutIteratorPtr((void*) array, elem_count, false);
+    iter.base.mutating = false;
+    return iter;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ucx/json.c	Sun Jan 05 22:00:39 2025 +0100
@@ -0,0 +1,1212 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/json.h"
+#include "cx/compare.h"
+
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <stdio.h>
+#include <errno.h>
+#include <inttypes.h>
+
+/*
+ * RFC 8259
+ * https://tools.ietf.org/html/rfc8259
+ */
+
+static CxJsonValue cx_json_value_nothing = {.type = CX_JSON_NOTHING};
+
+static int json_cmp_objvalue(const void *l, const void *r) {
+    const CxJsonObjValue *left = l;
+    const CxJsonObjValue *right = r;
+    return cx_strcmp(cx_strcast(left->name), cx_strcast(right->name));
+}
+
+static CxJsonObjValue *json_find_objvalue(const CxJsonValue *obj, cxstring name) {
+    assert(obj->type == CX_JSON_OBJECT);
+    CxJsonObjValue kv_dummy;
+    kv_dummy.name = cx_mutstrn((char*) name.ptr, name.length);
+    size_t index = cx_array_binary_search(
+            obj->value.object.values,
+            obj->value.object.values_size,
+            sizeof(CxJsonObjValue),
+            &kv_dummy,
+            json_cmp_objvalue
+    );
+    if (index == obj->value.object.values_size) {
+        return NULL;
+    } else {
+        return &obj->value.object.values[index];
+    }
+}
+
+static int json_add_objvalue(CxJsonValue *objv, CxJsonObjValue member) {
+    assert(objv->type == CX_JSON_OBJECT);
+    const CxAllocator * const al = objv->allocator;
+    CxJsonObject *obj = &(objv->value.object);
+
+    // determine the index where we need to insert the new member
+    size_t index = cx_array_binary_search_sup(
+        obj->values,
+        obj->values_size,
+        sizeof(CxJsonObjValue),
+        &member, json_cmp_objvalue
+    );
+
+    // is the name already present?
+    if (index < obj->values_size && 0 == json_cmp_objvalue(&member, &obj->values[index])) {
+        // free the original value
+        cx_strfree_a(al, &obj->values[index].name);
+        cxJsonValueFree(obj->values[index].value);
+        // replace the item
+        obj->values[index] = member;
+
+        // nothing more to do
+        return 0;
+    }
+
+    // determine the old capacity and reserve for one more element
+    CxArrayReallocator arealloc = cx_array_reallocator(al, NULL);
+    size_t oldcap = obj->values_capacity;
+    if (cx_array_simple_reserve_a(&arealloc, obj->values, 1)) return 1;
+
+    // check the new capacity, if we need to realloc the index array
+    size_t newcap = obj->values_capacity;
+    if (newcap > oldcap) {
+        if (cxReallocateArray(al, &obj->indices, newcap, sizeof(size_t))) {
+            return 1;
+        }
+    }
+
+    // check if append or insert
+    if (index < obj->values_size) {
+        // move the other elements
+        memmove(
+            &obj->values[index+1],
+            &obj->values[index],
+            (obj->values_size - index) * sizeof(CxJsonObjValue)
+        );
+        // increase indices for the moved elements
+        for (size_t i = 0; i < obj->values_size ; i++) {
+            if (obj->indices[i] >= index) {
+                obj->indices[i]++;
+            }
+        }
+    }
+
+    // insert the element and set the index
+    obj->values[index] = member;
+    obj->indices[obj->values_size] = index;
+    obj->values_size++;
+
+    return 0;
+}
+
+static void token_destroy(CxJsonToken *token) {
+    if (token->allocated) {
+        cx_strfree(&token->content);
+    }
+}
+
+static int num_isexp(const char *content, size_t length, size_t pos) {
+    if (pos >= length) {
+        return 0;
+    }
+
+    int ok = 0;
+    for (size_t i = pos; i < length; i++) {
+        char c = content[i];
+        if (isdigit(c)) {
+            ok = 1;
+        } else if (i == pos) {
+            if (!(c == '+' || c == '-')) {
+                return 0;
+            }
+        } else {
+            return 0;
+        }
+    }
+
+    return ok;
+}
+
+static CxJsonTokenType token_numbertype(const char *content, size_t length) {
+    if (length == 0) return CX_JSON_TOKEN_ERROR;
+
+    if (content[0] != '-' && !isdigit(content[0])) {
+        return CX_JSON_TOKEN_ERROR;
+    }
+
+    CxJsonTokenType type = CX_JSON_TOKEN_INTEGER;
+    for (size_t i = 1; i < length; i++) {
+        if (content[i] == '.') {
+            if (type == CX_JSON_TOKEN_NUMBER) {
+                return CX_JSON_TOKEN_ERROR; // more than one decimal separator
+            }
+            type = CX_JSON_TOKEN_NUMBER;
+        } else if (content[i] == 'e' || content[i] == 'E') {
+            return num_isexp(content, length, i + 1) ? CX_JSON_TOKEN_NUMBER : CX_JSON_TOKEN_ERROR;
+        } else if (!isdigit(content[i])) {
+            return CX_JSON_TOKEN_ERROR; // char is not a digit, decimal separator or exponent sep
+        }
+    }
+
+    return type;
+}
+
+static CxJsonToken token_create(CxJson *json, bool isstring, size_t start, size_t end) {
+    cxmutstr str = cx_mutstrn(json->buffer.space + start, end - start);
+    bool allocated = false;
+    if (json->uncompleted.tokentype != CX_JSON_NO_TOKEN) {
+        allocated = true;
+        str = cx_strcat_m(json->uncompleted.content, 1, str);
+        if (str.ptr == NULL) { // LCOV_EXCL_START
+            return (CxJsonToken){CX_JSON_NO_TOKEN, false, {NULL, 0}};
+        } // LCOV_EXCL_STOP
+    }
+    json->uncompleted = (CxJsonToken){0};
+    CxJsonTokenType ttype;
+    if (isstring) {
+        ttype = CX_JSON_TOKEN_STRING;
+    } else {
+        cxstring s = cx_strcast(str);
+        if (!cx_strcmp(s, CX_STR("true")) || !cx_strcmp(s, CX_STR("false"))
+            || !cx_strcmp(s, CX_STR("null"))) {
+            ttype = CX_JSON_TOKEN_LITERAL;
+        } else {
+            ttype = token_numbertype(str.ptr, str.length);
+        }
+    }
+    if (ttype == CX_JSON_TOKEN_ERROR) {
+        if (allocated) {
+            cx_strfree(&str);
+        }
+        return (CxJsonToken){CX_JSON_TOKEN_ERROR, false, {NULL, 0}};
+    }
+    return (CxJsonToken){ttype, allocated, str};
+}
+
+static CxJsonTokenType char2ttype(char c) {
+    switch (c) {
+        case '[': {
+            return CX_JSON_TOKEN_BEGIN_ARRAY;
+        }
+        case '{': {
+            return CX_JSON_TOKEN_BEGIN_OBJECT;
+        }
+        case ']': {
+            return CX_JSON_TOKEN_END_ARRAY;
+        }
+        case '}': {
+            return CX_JSON_TOKEN_END_OBJECT;
+        }
+        case ':': {
+            return CX_JSON_TOKEN_NAME_SEPARATOR;
+        }
+        case ',': {
+            return CX_JSON_TOKEN_VALUE_SEPARATOR;
+        }
+        case '"': {
+            return CX_JSON_TOKEN_STRING;
+        }
+        default: {
+            if (isspace(c)) {
+                return CX_JSON_TOKEN_SPACE;
+            }
+        }
+    }
+    return CX_JSON_NO_TOKEN;
+}
+
+static enum cx_json_status token_parse_next(CxJson *json, CxJsonToken *result) {
+    // check if there is data in the buffer
+    if (cxBufferEof(&json->buffer)) {
+        return json->uncompleted.tokentype == CX_JSON_NO_TOKEN ?
+            CX_JSON_NO_DATA : CX_JSON_INCOMPLETE_DATA;
+    }
+
+    // current token type and start index
+    CxJsonTokenType ttype = json->uncompleted.tokentype;
+    size_t token_start = json->buffer.pos;
+
+    for (size_t i = json->buffer.pos; i < json->buffer.size; i++) {
+        char c = json->buffer.space[i];
+        if (ttype != CX_JSON_TOKEN_STRING) {
+            // currently non-string token
+            CxJsonTokenType ctype = char2ttype(c); // start of new token?
+            if (ttype == CX_JSON_NO_TOKEN) {
+                if (ctype == CX_JSON_TOKEN_SPACE) {
+                    json->buffer.pos++;
+                    continue;
+                } else if (ctype == CX_JSON_TOKEN_STRING) {
+                    // begin string
+                    ttype = CX_JSON_TOKEN_STRING;
+                    token_start = i;
+                } else if (ctype != CX_JSON_NO_TOKEN) {
+                    // single-char token
+                    json->buffer.pos = i + 1;
+                    *result = (CxJsonToken){ctype, false, {NULL, 0}};
+                    return CX_JSON_NO_ERROR;
+                } else {
+                    ttype = CX_JSON_TOKEN_LITERAL; // number or literal
+                    token_start = i;
+                }
+            } else {
+                // finish token
+                if (ctype != CX_JSON_NO_TOKEN) {
+                    *result = token_create(json, false, token_start, i);
+                    if (result->tokentype == CX_JSON_NO_TOKEN) {
+                        return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+                    }
+                    if (result->tokentype == CX_JSON_TOKEN_ERROR) {
+                        return CX_JSON_FORMAT_ERROR_NUMBER;
+                    }
+                    json->buffer.pos = i;
+                    return CX_JSON_NO_ERROR;
+                }
+            }
+        } else {
+            // currently inside a string
+            if (json->tokenizer_escape) {
+                json->tokenizer_escape = false;
+            } else {
+                if (c == '"') {
+                    *result = token_create(json, true, token_start, i + 1);
+                    if (result->tokentype == CX_JSON_NO_TOKEN) {
+                        return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+                    }
+                    json->buffer.pos = i + 1;
+                    return CX_JSON_NO_ERROR;
+                } else if (c == '\\') {
+                    json->tokenizer_escape = true;
+                }
+            }
+        }
+    }
+
+    if (ttype != CX_JSON_NO_TOKEN) {
+        // uncompleted token
+        size_t uncompleted_len = json->buffer.size - token_start;
+        if (json->uncompleted.tokentype == CX_JSON_NO_TOKEN) {
+            // current token is uncompleted
+            // save current token content
+            CxJsonToken uncompleted = {
+                ttype, true,
+                cx_strdup(cx_strn(json->buffer.space + token_start, uncompleted_len))
+            };
+            if (uncompleted.content.ptr == NULL) {
+                return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+            }
+            json->uncompleted = uncompleted;
+        } else {
+            // previously we also had an uncompleted token
+            // combine the uncompleted token with the current token
+            assert(json->uncompleted.allocated);
+            cxmutstr str = cx_strcat_m(json->uncompleted.content, 1,
+                cx_strn(json->buffer.space + token_start, uncompleted_len));
+            if (str.ptr == NULL) {
+                return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+            }
+            json->uncompleted.content = str;
+        }
+        // advance the buffer position - we saved the stuff in the uncompleted token
+        json->buffer.pos += uncompleted_len;
+    }
+
+    return CX_JSON_INCOMPLETE_DATA;
+}
+
+static cxmutstr unescape_string(const CxAllocator *a, cxmutstr str) {
+    // TODO: support more escape sequences
+    // we know that the unescaped string will be shorter by at least 2 chars
+    cxmutstr result;
+    result.length = 0;
+    result.ptr = cxMalloc(a, str.length - 1);
+    if (result.ptr == NULL) return result; // LCOV_EXCL_LINE
+
+    bool u = false;
+    for (size_t i = 1; i < str.length - 1; i++) {
+        char c = str.ptr[i];
+        if (u) {
+            u = false;
+            if (c == 'n') {
+                c = '\n';
+            } else if (c == 't') {
+                c = '\t';
+            }
+            result.ptr[result.length++] = c;
+        } else {
+            if (c == '\\') {
+                u = true;
+            } else {
+                result.ptr[result.length++] = c;
+            }
+        }
+    }
+    result.ptr[result.length] = 0;
+
+    return result;
+}
+
+static CxJsonValue* create_json_value(CxJson *json, CxJsonValueType type) {
+    CxJsonValue *v = cxCalloc(json->allocator, 1, sizeof(CxJsonValue));
+    if (v == NULL) return NULL; // LCOV_EXCL_LINE
+
+    // initialize the value
+    v->type = type;
+    v->allocator = json->allocator;
+    if (type == CX_JSON_ARRAY) {
+        cx_array_initialize_a(json->allocator, v->value.array.array, 16);
+        if (v->value.array.array == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
+    } else if (type == CX_JSON_OBJECT) {
+        cx_array_initialize_a(json->allocator, v->value.object.values, 16);
+        v->value.object.indices = cxCalloc(json->allocator, 16, sizeof(size_t));
+        if (v->value.object.values == NULL ||
+            v->value.object.indices == NULL)
+            goto create_json_value_exit_error; // LCOV_EXCL_LINE
+    }
+
+    // add the new value to a possible parent
+    if (json->vbuf_size > 0) {
+        CxJsonValue *parent = json->vbuf[json->vbuf_size - 1];
+        assert(parent != NULL);
+        if (parent->type == CX_JSON_ARRAY) {
+            CxArrayReallocator value_realloc = cx_array_reallocator(json->allocator, NULL);
+            if (cx_array_simple_add_a(&value_realloc, parent->value.array.array, v)) {
+                goto create_json_value_exit_error; // LCOV_EXCL_LINE
+            }
+        } else if (parent->type == CX_JSON_OBJECT) {
+            // the member was already created after parsing the name
+            assert(json->uncompleted_member.name.ptr != NULL);
+            json->uncompleted_member.value = v;
+            if (json_add_objvalue(parent, json->uncompleted_member))  {
+                goto create_json_value_exit_error; // LCOV_EXCL_LINE
+            }
+            json->uncompleted_member.name = (cxmutstr) {NULL, 0};
+        } else {
+            assert(false); // LCOV_EXCL_LINE
+        }
+    }
+
+    // add the new value to the stack, if it is an array or object
+    if (type == CX_JSON_ARRAY || type == CX_JSON_OBJECT) {
+        CxArrayReallocator vbuf_realloc = cx_array_reallocator(NULL, json->vbuf_internal);
+        if (cx_array_simple_add_a(&vbuf_realloc, json->vbuf, v)) {
+            goto create_json_value_exit_error; // LCOV_EXCL_LINE
+        }
+    }
+
+    // if currently no value is parsed, this is now the value of interest
+    if (json->parsed == NULL) {
+        json->parsed = v;
+    }
+
+    return v;
+    // LCOV_EXCL_START
+create_json_value_exit_error:
+    cxJsonValueFree(v);
+    return NULL;
+    // LCOV_EXCL_STOP
+}
+
+#define JP_STATE_VALUE_BEGIN         0
+#define JP_STATE_VALUE_END          10
+#define JP_STATE_VALUE_BEGIN_OBJ     1
+#define JP_STATE_OBJ_SEP_OR_CLOSE   11
+#define JP_STATE_VALUE_BEGIN_AR      2
+#define JP_STATE_ARRAY_SEP_OR_CLOSE 12
+#define JP_STATE_OBJ_NAME_OR_CLOSE   5
+#define JP_STATE_OBJ_NAME            6
+#define JP_STATE_OBJ_COLON           7
+
+void cxJsonInit(CxJson *json, const CxAllocator *allocator) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
+    }
+    
+    memset(json, 0, sizeof(CxJson));
+    json->allocator = allocator;
+
+    json->states = json->states_internal;
+    json->states_capacity = cx_nmemb(json->states_internal);
+    json->states[0] = JP_STATE_VALUE_BEGIN;
+    json->states_size = 1;
+
+    json->vbuf = json->vbuf_internal;
+    json->vbuf_capacity = cx_nmemb(json->vbuf_internal);
+}
+
+void cxJsonDestroy(CxJson *json) {
+    cxBufferDestroy(&json->buffer);
+    if (json->states != json->states_internal) {
+        free(json->states);
+    }
+    if (json->vbuf != json->vbuf_internal) {
+        free(json->vbuf);
+    }
+    cxJsonValueFree(json->parsed);
+    json->parsed = NULL;
+    if (json->uncompleted_member.name.ptr != NULL) {
+        cx_strfree_a(json->allocator, &json->uncompleted_member.name);
+        json->uncompleted_member = (CxJsonObjValue){{NULL, 0}, NULL};
+    }
+}
+
+int cxJsonFilln(CxJson *json, const char *buf, size_t size) {
+    if (cxBufferEof(&json->buffer)) {
+        // reinitialize the buffer
+        cxBufferDestroy(&json->buffer);
+        cxBufferInit(&json->buffer, (char*) buf, size,
+            NULL, CX_BUFFER_AUTO_EXTEND | CX_BUFFER_COPY_ON_WRITE);
+        json->buffer.size = size;
+        return 0;
+    } else {
+        return size != cxBufferAppend(buf, 1, size, &json->buffer);
+    }
+}
+
+static void json_add_state(CxJson *json, int state) {
+    // we have guaranteed the necessary space with cx_array_simple_reserve()
+    // therefore, we can safely add the state in the simplest way possible
+    json->states[json->states_size++] = state;
+}
+
+#define return_rec(code) \
+    token_destroy(&token); \
+    return code
+
+static enum cx_json_status json_parse(CxJson *json) {
+    // Reserve a pointer for a possibly read value
+    CxJsonValue *vbuf = NULL;
+
+    // grab the next token
+    CxJsonToken token;
+    {
+        enum cx_json_status ret = token_parse_next(json, &token);
+        if (ret != CX_JSON_NO_ERROR) {
+            return ret;
+        }
+    }
+
+    // pop the current state
+    assert(json->states_size > 0);
+    int state = json->states[--json->states_size];
+
+    // guarantee that at least two more states fit on the stack
+    CxArrayReallocator state_realloc = cx_array_reallocator(NULL, json->states_internal);
+    if (cx_array_simple_reserve_a(&state_realloc, json->states, 2)) {
+        return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
+    }
+
+
+    //  0 JP_STATE_VALUE_BEGIN          value begin
+    // 10 JP_STATE_VALUE_END            expect value end
+    //  1 JP_STATE_VALUE_BEGIN_OBJ      value begin (inside object)
+    // 11 JP_STATE_OBJ_SEP_OR_CLOSE     object, expect separator, objclose
+    //  2 JP_STATE_VALUE_BEGIN_AR       value begin (inside array)
+    // 12 JP_STATE_ARRAY_SEP_OR_CLOSE   array, expect separator or arrayclose
+    //  5 JP_STATE_OBJ_NAME_OR_CLOSE    object, expect name or objclose
+    //  6 JP_STATE_OBJ_NAME             object, expect name
+    //  7 JP_STATE_OBJ_COLON            object, expect ':'
+
+    if (state < 3) {
+        // push expected end state to the stack
+        json_add_state(json, 10 + state);
+        switch (token.tokentype) {
+            case CX_JSON_TOKEN_BEGIN_ARRAY: {
+                if (create_json_value(json, CX_JSON_ARRAY) == NULL) {
+                    return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
+                }
+                json_add_state(json, JP_STATE_VALUE_BEGIN_AR);
+                return_rec(CX_JSON_NO_ERROR);
+            }
+            case CX_JSON_TOKEN_BEGIN_OBJECT: {
+                if (create_json_value(json, CX_JSON_OBJECT) == NULL) {
+                    return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
+                }
+                json_add_state(json, JP_STATE_OBJ_NAME_OR_CLOSE);
+                return_rec(CX_JSON_NO_ERROR);
+            }
+            case CX_JSON_TOKEN_STRING: {
+                if ((vbuf = create_json_value(json, CX_JSON_STRING)) == NULL) {
+                    return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
+                }
+                cxmutstr str = unescape_string(json->allocator, token.content);
+                if (str.ptr == NULL) {
+                    return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
+                }
+                vbuf->value.string = str;
+                return_rec(CX_JSON_NO_ERROR);
+            }
+            case CX_JSON_TOKEN_INTEGER:
+            case CX_JSON_TOKEN_NUMBER: {
+                int type = token.tokentype == CX_JSON_TOKEN_INTEGER ? CX_JSON_INTEGER : CX_JSON_NUMBER;
+                if (NULL == (vbuf = create_json_value(json, type))) {
+                    return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
+                }
+                if (type == CX_JSON_INTEGER) {
+                    if (cx_strtoi64(token.content, &vbuf->value.integer, 10)) {
+                        return_rec(CX_JSON_FORMAT_ERROR_NUMBER);
+                    }
+                } else {
+                    if (cx_strtod(token.content, &vbuf->value.number)) {
+                        return_rec(CX_JSON_FORMAT_ERROR_NUMBER);
+                    }
+                }
+                return_rec(CX_JSON_NO_ERROR);
+            }
+            case CX_JSON_TOKEN_LITERAL: {
+                if ((vbuf = create_json_value(json, CX_JSON_LITERAL)) == NULL) {
+                    return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
+                }
+                if (0 == cx_strcmp(cx_strcast(token.content), cx_str("true"))) {
+                    vbuf->value.literal = CX_JSON_TRUE;
+                } else if (0 == cx_strcmp(cx_strcast(token.content), cx_str("false"))) {
+                    vbuf->value.literal = CX_JSON_FALSE;
+                } else {
+                    vbuf->value.literal = CX_JSON_NULL;
+                }
+                return_rec(CX_JSON_NO_ERROR);
+            }
+            default: {
+                return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN);
+            }
+        }
+    } else if (state == JP_STATE_ARRAY_SEP_OR_CLOSE) {
+        // expect ',' or ']'
+        if (token.tokentype == CX_JSON_TOKEN_VALUE_SEPARATOR) {
+            json_add_state(json, JP_STATE_VALUE_BEGIN_AR);
+            return_rec(CX_JSON_NO_ERROR);
+        } else if (token.tokentype == CX_JSON_TOKEN_END_ARRAY) {
+            // discard the array from the value buffer
+            json->vbuf_size--;
+            return_rec(CX_JSON_NO_ERROR);
+        } else {
+            return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN);
+        }
+    } else if (state == JP_STATE_OBJ_NAME_OR_CLOSE || state == JP_STATE_OBJ_NAME) {
+        if (state == JP_STATE_OBJ_NAME_OR_CLOSE && token.tokentype == CX_JSON_TOKEN_END_OBJECT) {
+            // discard the obj from the value buffer
+            json->vbuf_size--;
+            return_rec(CX_JSON_NO_ERROR);
+        } else {
+            // expect string
+            if (token.tokentype != CX_JSON_TOKEN_STRING) {
+                return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN);
+            }
+
+            // add new entry
+            cxmutstr name = unescape_string(json->allocator, token.content);
+            if (name.ptr == NULL) {
+                return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
+            }
+            assert(json->uncompleted_member.name.ptr == NULL);
+            json->uncompleted_member.name = name;
+            assert(json->vbuf_size > 0);
+
+            // next state
+            json_add_state(json, JP_STATE_OBJ_COLON);
+            return_rec(CX_JSON_NO_ERROR);
+        }
+    } else if (state == JP_STATE_OBJ_COLON) {
+        // expect ':'
+        if (token.tokentype != CX_JSON_TOKEN_NAME_SEPARATOR) {
+            return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN);
+        }
+        // next state
+        json_add_state(json, JP_STATE_VALUE_BEGIN_OBJ);
+        return_rec(CX_JSON_NO_ERROR);
+    } else if (state == JP_STATE_OBJ_SEP_OR_CLOSE) {
+        // expect ',' or '}'
+        if (token.tokentype == CX_JSON_TOKEN_VALUE_SEPARATOR) {
+            json_add_state(json, JP_STATE_OBJ_NAME);
+            return_rec(CX_JSON_NO_ERROR);
+        } else if (token.tokentype == CX_JSON_TOKEN_END_OBJECT) {
+            // discard the obj from the value buffer
+            json->vbuf_size--;
+            return_rec(CX_JSON_NO_ERROR);
+        } else {
+            return_rec(CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN);
+        }
+    } else {
+        // should be unreachable
+        assert(false);
+        return_rec(-1);
+    }
+}
+
+CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value) {
+    // check if buffer has been filled
+    if (json->buffer.space == NULL) {
+        return CX_JSON_NULL_DATA;
+    }
+
+    // initialize output value
+    *value = &cx_json_value_nothing;
+
+    // parse data
+    CxJsonStatus result;
+    do {
+        result = json_parse(json);
+        if (result == CX_JSON_NO_ERROR && json->states_size == 1) {
+            // final state reached
+            assert(json->states[0] == JP_STATE_VALUE_END);
+            assert(json->vbuf_size == 0);
+
+            // write output value
+            *value = json->parsed;
+            json->parsed = NULL;
+
+            // re-initialize state machine
+            json->states[0] = JP_STATE_VALUE_BEGIN;
+
+            return CX_JSON_NO_ERROR;
+        }
+    } while (result == CX_JSON_NO_ERROR);
+
+    // the parser might think there is no data
+    // but when we did not reach the final state,
+    // we know that there must be more to come
+    if (result == CX_JSON_NO_DATA && json->states_size > 1) {
+        return CX_JSON_INCOMPLETE_DATA;
+    }
+
+    return result;
+}
+
+void cxJsonValueFree(CxJsonValue *value) {
+    if (value == NULL || value->type == CX_JSON_NOTHING) return;
+    switch (value->type) {
+        case CX_JSON_OBJECT: {
+            CxJsonObject obj = value->value.object;
+            for (size_t i = 0; i < obj.values_size; i++) {
+                cxJsonValueFree(obj.values[i].value);
+                cx_strfree_a(value->allocator, &obj.values[i].name);
+            }
+            cxFree(value->allocator, obj.values);
+            cxFree(value->allocator, obj.indices);
+            break;
+        }
+        case CX_JSON_ARRAY: {
+            CxJsonArray array = value->value.array;
+            for (size_t i = 0; i < array.array_size; i++) {
+                cxJsonValueFree(array.array[i]);
+            }
+            cxFree(value->allocator, array.array);
+            break;
+        }
+        case CX_JSON_STRING: {
+            cxFree(value->allocator, value->value.string.ptr);
+            break;
+        }
+        default: {
+            break;
+        }
+    }
+    cxFree(value->allocator, value);
+}
+
+CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator) {
+    CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
+    if (v == NULL) return NULL;
+    v->allocator = allocator;
+    v->type = CX_JSON_OBJECT;
+    cx_array_initialize_a(allocator, v->value.object.values, 16);
+    if (v->value.object.values == NULL) { // LCOV_EXCL_START
+        cxFree(allocator, v);
+        return NULL;
+        // LCOV_EXCL_STOP
+    }
+    v->value.object.indices = cxCalloc(allocator, 16, sizeof(size_t));
+    if (v->value.object.indices == NULL) { // LCOV_EXCL_START
+        cxFree(allocator, v->value.object.values);
+        cxFree(allocator, v);
+        return NULL;
+        // LCOV_EXCL_STOP
+    }
+    return v;
+}
+
+CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator) {
+    CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
+    if (v == NULL) return NULL;
+    v->allocator = allocator;
+    v->type = CX_JSON_ARRAY;
+    cx_array_initialize_a(allocator, v->value.array.array, 16);
+    if (v->value.array.array == NULL) { cxFree(allocator, v); return NULL; }
+    return v;
+}
+
+CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num) {
+    CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
+    if (v == NULL) return NULL;
+    v->allocator = allocator;
+    v->type = CX_JSON_NUMBER;
+    v->value.number = num;
+    return v;
+}
+
+CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num) {
+    CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
+    if (v == NULL) return NULL;
+    v->allocator = allocator;
+    v->type = CX_JSON_INTEGER;
+    v->value.integer = num;
+    return v;
+}
+
+CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char* str) {
+    return cxJsonCreateCxString(allocator, cx_str(str));
+}
+
+CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str) {
+    CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
+    if (v == NULL) return NULL;
+    v->allocator = allocator;
+    v->type = CX_JSON_STRING;
+    cxmutstr s = cx_strdup_a(allocator, str);
+    if (s.ptr == NULL) { cxFree(allocator, v); return NULL; }
+    v->value.string = s;
+    return v;
+}
+
+CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit) {
+    CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
+    if (v == NULL) return NULL;
+    v->allocator = allocator;
+    v->type = CX_JSON_LITERAL;
+    v->value.literal = lit;
+    return v;
+}
+
+// LCOV_EXCL_START
+// never called as long as malloc() does not return NULL
+static void cx_json_arr_free_temp(CxJsonValue** values, size_t count) {
+    for (size_t i = 0; i < count; i++) {
+        if (values[i] == NULL) break;
+        cxJsonValueFree(values[i]);
+    }
+    free(values);
+}
+// LCOV_EXCL_STOP
+
+int cxJsonArrAddNumbers(CxJsonValue* arr, const double* num, size_t count) {
+    CxJsonValue** values = calloc(count, sizeof(CxJsonValue*));
+    if (values == NULL) return -1;
+    for (size_t i = 0; i < count; i++) {
+        values[i] = cxJsonCreateNumber(arr->allocator, num[i]);
+        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+    }
+    int ret = cxJsonArrAddValues(arr, values, count);
+    free(values);
+    return ret;
+}
+
+int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count) {
+    CxJsonValue** values = calloc(count, sizeof(CxJsonValue*));
+    if (values == NULL) return -1;
+    for (size_t i = 0; i < count; i++) {
+        values[i] = cxJsonCreateInteger(arr->allocator, num[i]);
+        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+    }
+    int ret = cxJsonArrAddValues(arr, values, count);
+    free(values);
+    return ret;
+}
+
+int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count) {
+    CxJsonValue** values = calloc(count, sizeof(CxJsonValue*));
+    if (values == NULL) return -1;
+    for (size_t i = 0; i < count; i++) {
+        values[i] = cxJsonCreateString(arr->allocator, str[i]);
+        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+    }
+    int ret = cxJsonArrAddValues(arr, values, count);
+    free(values);
+    return ret;
+}
+
+int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count) {
+    CxJsonValue** values = calloc(count, sizeof(CxJsonValue*));
+    if (values == NULL) return -1;
+    for (size_t i = 0; i < count; i++) {
+        values[i] = cxJsonCreateCxString(arr->allocator, str[i]);
+        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+    }
+    int ret = cxJsonArrAddValues(arr, values, count);
+    free(values);
+    return ret;
+}
+
+int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count) {
+    CxJsonValue** values = calloc(count, sizeof(CxJsonValue*));
+    if (values == NULL) return -1;
+    for (size_t i = 0; i < count; i++) {
+        values[i] = cxJsonCreateLiteral(arr->allocator, lit[i]);
+        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+    }
+    int ret = cxJsonArrAddValues(arr, values, count);
+    free(values);
+    return ret;
+}
+
+int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count) {
+    CxArrayReallocator value_realloc = cx_array_reallocator(arr->allocator, NULL);
+    assert(arr->type == CX_JSON_ARRAY);
+    return cx_array_simple_copy_a(&value_realloc,
+            arr->value.array.array,
+            arr->value.array.array_size,
+            val, count
+    );
+}
+
+int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child) {
+    cxmutstr k = cx_strdup_a(obj->allocator, name);
+    if (k.ptr == NULL) return -1;
+    CxJsonObjValue kv = {k, child};
+    if (json_add_objvalue(obj, kv)) {
+        cx_strfree_a(obj->allocator, &k);
+        return 1;
+    } else {
+        return 0;
+    }
+}
+
+CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name) {
+    CxJsonValue* v = cxJsonCreateObj(obj->allocator);
+    if (v == NULL) return NULL;
+    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
+    return v;
+}
+
+CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name) {
+    CxJsonValue* v = cxJsonCreateArr(obj->allocator);
+    if (v == NULL) return NULL;
+    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
+    return v;
+}
+
+CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num) {
+    CxJsonValue* v = cxJsonCreateNumber(obj->allocator, num);
+    if (v == NULL) return NULL;
+    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
+    return v;
+}
+
+CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num) {
+    CxJsonValue* v = cxJsonCreateInteger(obj->allocator, num);
+    if (v == NULL) return NULL;
+    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
+    return v;
+}
+
+CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str) {
+    CxJsonValue* v = cxJsonCreateString(obj->allocator, str);
+    if (v == NULL) return NULL;
+    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
+    return v;
+}
+
+CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str) {
+    CxJsonValue* v = cxJsonCreateCxString(obj->allocator, str);
+    if (v == NULL) return NULL;
+    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL; }
+    return v;
+}
+
+CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit) {
+    CxJsonValue* v = cxJsonCreateLiteral(obj->allocator, lit);
+    if (v == NULL) return NULL;
+    if (cxJsonObjPut(obj, name, v)) { cxJsonValueFree(v); return NULL;}
+    return v;
+}
+
+CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index) {
+    if (index >= value->value.array.array_size) {
+        return &cx_json_value_nothing;
+    }
+    return value->value.array.array[index];
+}
+
+CxIterator cxJsonArrIter(const CxJsonValue *value) {
+    return cxIteratorPtr(
+        value->value.array.array,
+        value->value.array.array_size
+    );
+}
+
+CxIterator cxJsonObjIter(const CxJsonValue *value) {
+    return cxIterator(
+        value->value.object.values,
+        sizeof(CxJsonObjValue),
+        value->value.object.values_size
+    );
+}
+
+CxJsonValue *cx_json_obj_get_cxstr(const CxJsonValue *value, cxstring name) {
+    CxJsonObjValue *member = json_find_objvalue(value, name);
+    if (member == NULL) {
+        return &cx_json_value_nothing;
+    } else {
+        return member->value;
+    }
+}
+
+static const CxJsonWriter cx_json_writer_default = {
+    false,
+    true,
+    255,
+    false,
+    4
+};
+
+CxJsonWriter cxJsonWriterCompact(void) {
+    return cx_json_writer_default;
+}
+
+CxJsonWriter cxJsonWriterPretty(bool use_spaces) {
+    return (CxJsonWriter) {
+        true,
+        true,
+        255,
+        use_spaces,
+        4
+    };
+}
+
+static int cx_json_writer_indent(
+    void *target,
+    cx_write_func wfunc,
+    const CxJsonWriter *settings,
+    unsigned int depth
+) {
+    if (depth == 0) return 0;
+
+    // determine the width and characters to use
+    const char* indent; // for 32 prepared chars
+    size_t width = depth;
+    if (settings->indent_space) {
+        if (settings->indent == 0) return 0;
+        width *= settings->indent;
+        indent = "                                ";
+    } else {
+        indent = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
+    }
+
+    // calculate the number of write calls and write
+    size_t full = width / 32;
+    size_t remaining = width % 32;
+    for (size_t i = 0; i < full; i++) {
+        if (32 != wfunc(indent, 1, 32, target)) return 1;
+    }
+    if (remaining != wfunc(indent, 1, remaining, target)) return 1;
+
+    return 0;
+}
+
+
+int cx_json_write_rec(
+    void *target,
+    const CxJsonValue *value,
+    cx_write_func wfunc,
+    const CxJsonWriter *settings,
+    unsigned int depth
+) {
+    // keep track of written items
+    // the idea is to reduce the number of jumps for error checking
+    size_t actual = 0, expected = 0;
+
+    // small buffer for number to string conversions
+    char numbuf[32];
+
+    // recursively write the values
+    switch (value->type) {
+        case CX_JSON_OBJECT: {
+            const char *begin_obj = "{\n";
+            if (settings->pretty) {
+                actual += wfunc(begin_obj, 1, 2, target);
+                expected += 2;
+            } else {
+                actual += wfunc(begin_obj, 1, 1, target);
+                expected++;
+            }
+            depth++;
+            size_t elem_count = value->value.object.values_size;
+            for (size_t look_idx = 0; look_idx < elem_count; look_idx++) {
+                // get the member either via index array or directly
+                size_t elem_idx = settings->sort_members
+                                      ? look_idx
+                                      : value->value.object.indices[look_idx];
+                CxJsonObjValue *member = &value->value.object.values[elem_idx];
+                if (settings->sort_members) {
+                    depth++;depth--;
+                }
+
+                // possible indentation
+                if (settings->pretty) {
+                    if (cx_json_writer_indent(target, wfunc, settings, depth)) {
+                        return 1; // LCOV_EXCL_LINE
+                    }
+                }
+
+                // the name
+                actual += wfunc("\"", 1, 1, target);
+                // TODO: escape the string
+                actual += wfunc(member->name.ptr, 1,
+                    member->name.length, target);
+                actual += wfunc("\"", 1, 1, target);
+                const char *obj_name_sep = ": ";
+                if (settings->pretty) {
+                    actual += wfunc(obj_name_sep, 1, 2, target);
+                    expected += 4 + member->name.length;
+                } else {
+                    actual += wfunc(obj_name_sep, 1, 1, target);
+                    expected += 3 + member->name.length;
+                }
+
+                // the value
+                if (cx_json_write_rec(target, member->value, wfunc, settings, depth)) return 1;
+
+                // end of object-value
+                if (look_idx < elem_count - 1) {
+                    const char *obj_value_sep = ",\n";
+                    if (settings->pretty) {
+                        actual += wfunc(obj_value_sep, 1, 2, target);
+                        expected += 2;
+                    } else {
+                        actual += wfunc(obj_value_sep, 1, 1, target);
+                        expected++;
+                    }
+                } else {
+                    if (settings->pretty) {
+                        actual += wfunc("\n", 1, 1, target);
+                        expected ++;
+                    }
+                }
+            }
+            depth--;
+            if (settings->pretty) {
+                if (cx_json_writer_indent(target, wfunc, settings, depth)) return 1;
+            }
+            actual += wfunc("}", 1, 1, target);
+            expected++;
+            break;
+        }
+        case CX_JSON_ARRAY: {
+            actual += wfunc("[", 1, 1, target);
+            expected++;
+            CxIterator iter = cxJsonArrIter(value);
+            cx_foreach(CxJsonValue*, element, iter) {
+                if (cx_json_write_rec(
+                        target, element,
+                        wfunc, settings, depth)
+                ) return 1;
+
+                if (iter.index < iter.elem_count - 1) {
+                    const char *arr_value_sep = ", ";
+                    if (settings->pretty) {
+                        actual += wfunc(arr_value_sep, 1, 2, target);
+                        expected += 2;
+                    } else {
+                        actual += wfunc(arr_value_sep, 1, 1, target);
+                        expected++;
+                    }
+                }
+            }
+            actual += wfunc("]", 1, 1, target);
+            expected++;
+            break;
+        }
+        case CX_JSON_STRING: {
+            actual += wfunc("\"", 1, 1, target);
+            // TODO: escape the string
+            actual += wfunc(value->value.string.ptr, 1,
+                value->value.string.length, target);
+            actual += wfunc("\"", 1, 1, target);
+            expected += 2 + value->value.string.length;
+            break;
+        }
+        case CX_JSON_NUMBER: {
+            // TODO: locale bullshit
+            // TODO: formatting settings
+            snprintf(numbuf, 32, "%g", value->value.number);
+            size_t len = strlen(numbuf);
+            actual += wfunc(numbuf, 1, len, target);
+            expected += len;
+            break;
+        }
+        case CX_JSON_INTEGER: {
+            snprintf(numbuf, 32, "%" PRIi64, value->value.integer);
+            size_t len = strlen(numbuf);
+            actual += wfunc(numbuf, 1, len, target);
+            expected += len;
+            break;
+        }
+        case CX_JSON_LITERAL: {
+            if (value->value.literal == CX_JSON_TRUE) {
+                actual += wfunc("true", 1, 4, target);
+                expected += 4;
+            } else if (value->value.literal == CX_JSON_FALSE) {
+                actual += wfunc("false", 1, 5, target);
+                expected += 5;
+            } else {
+                actual += wfunc("null", 1, 4, target);
+                expected += 4;
+            }
+            break;
+        }
+        case CX_JSON_NOTHING: {
+            // deliberately supported as an empty string!
+            // users might want to just write the result
+            // of a get operation without testing the value
+            // and therefore this should not blow up
+            break;
+        }
+        default: assert(false); // LCOV_EXCL_LINE
+    }
+
+    return expected != actual;
+}
+
+int cxJsonWrite(
+    void *target,
+    const CxJsonValue *value,
+    cx_write_func wfunc,
+    const CxJsonWriter *settings
+) {
+    if (settings == NULL) {
+        settings = &cx_json_writer_default;
+    }
+    assert(target != NULL);
+    assert(value != NULL);
+    assert(wfunc != NULL);
+
+    return cx_json_write_rec(target, value, wfunc, settings, 0);
+}
--- a/ucx/linked_list.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/linked_list.c	Sun Jan 05 22:00:39 2025 +0100
@@ -27,7 +27,6 @@
  */
 
 #include "cx/linked_list.h"
-#include "cx/utils.h"
 #include "cx/compare.h"
 #include <string.h>
 #include <assert.h>
@@ -91,7 +90,7 @@
     do {
         void *current = ll_data(node);
         if (cmp_func(current, elem) == 0) {
-            *result = (void*) node;
+            *result = (void *) node;
             return index;
         }
         node = ll_advance(node);
@@ -336,19 +335,22 @@
     }
 }
 
-void cx_linked_list_remove(
+size_t cx_linked_list_remove_chain(
         void **begin,
         void **end,
         ptrdiff_t loc_prev,
         ptrdiff_t loc_next,
-        void *node
+        void *node,
+        size_t num
 ) {
     assert(node != NULL);
     assert(loc_next >= 0);
     assert(loc_prev >= 0 || begin != NULL);
 
+    // easy exit
+    if (num == 0) return 0;
+
     // find adjacent nodes
-    void *next = ll_next(node);
     void *prev;
     if (loc_prev >= 0) {
         prev = ll_prev(node);
@@ -356,6 +358,12 @@
         prev = cx_linked_list_prev(*begin, loc_next, node);
     }
 
+    void *next = ll_next(node);
+    size_t removed = 1;
+    for (; removed < num && next != NULL ; removed++) {
+        next = ll_next(next);
+    }
+
     // update next pointer of prev node, or set begin
     if (prev == NULL) {
         if (begin != NULL) {
@@ -373,6 +381,8 @@
     } else if (loc_prev >= 0) {
         ll_prev(next) = prev;
     }
+
+    return removed;
 }
 
 size_t cx_linked_list_size(
@@ -436,13 +446,13 @@
 
     // Update pointer
     if (loc_prev >= 0) ll_prev(sorted[0]) = NULL;
-    cx_for_n (i, length - 1) {
+    for (size_t i = 0 ; i < length - 1; i++) {
         cx_linked_list_link(sorted[i], sorted[i + 1], loc_prev, loc_next);
     }
     ll_next(sorted[length - 1]) = NULL;
 
     *begin = sorted[0];
-    *end = sorted[length-1];
+    *end = sorted[length - 1];
     if (sorted != sbo) {
         free(sorted);
     }
@@ -646,9 +656,7 @@
     cx_linked_list_node *node = index == 0 ? NULL : cx_ll_node_at((cx_linked_list *) list, index - 1);
 
     // perform first insert
-    if (0 != cx_ll_insert_at(list, node, array)) {
-        return 1;
-    }
+    if (0 != cx_ll_insert_at(list, node, array)) return 1;
 
     // is there more?
     if (n == 1) return 1;
@@ -660,9 +668,7 @@
     const char *source = array;
     for (size_t i = 1; i < n; i++) {
         source += list->collection.elem_size;
-        if (0 != cx_ll_insert_at(list, node, source)) {
-            return i;
-        }
+        if (0 != cx_ll_insert_at(list, node, source)) return i;
         node = node->next;
     }
     return n;
@@ -733,30 +739,61 @@
     return inserted;
 }
 
-static int cx_ll_remove(
+static size_t cx_ll_remove(
         struct cx_list_s *list,
-        size_t index
+        size_t index,
+        size_t num,
+        void *targetbuf
 ) {
     cx_linked_list *ll = (cx_linked_list *) list;
     cx_linked_list_node *node = cx_ll_node_at(ll, index);
 
     // out-of-bounds check
-    if (node == NULL) return 1;
-
-    // element destruction
-    cx_invoke_destructor(list, node->payload);
+    if (node == NULL) return 0;
 
     // remove
-    cx_linked_list_remove((void **) &ll->begin, (void **) &ll->end,
-                          CX_LL_LOC_PREV, CX_LL_LOC_NEXT, node);
+    size_t removed = cx_linked_list_remove_chain(
+            (void **) &ll->begin,
+            (void **) &ll->end,
+            CX_LL_LOC_PREV,
+            CX_LL_LOC_NEXT,
+            node,
+            num
+    );
 
     // adjust size
-    list->collection.size--;
+    list->collection.size -= removed;
+
+    // copy or destroy the removed chain
+    if (targetbuf == NULL) {
+        cx_linked_list_node *n = node;
+        for (size_t i = 0; i < removed; i++) {
+            // element destruction
+            cx_invoke_destructor(list, n->payload);
 
-    // free and return
-    cxFree(list->collection.allocator, node);
+            // free the node and advance
+            void *next = n->next;
+            cxFree(list->collection.allocator, n);
+            n = next;
+        }
+    } else {
+        char *dest = targetbuf;
+        cx_linked_list_node *n = node;
+        for (size_t i = 0; i < removed; i++) {
+            // copy payload
+            memcpy(dest, n->payload, list->collection.elem_size);
 
-    return 0;
+            // advance target buffer
+            dest += list->collection.elem_size;
+
+            // free the node and advance
+            void *next = n->next;
+            cxFree(list->collection.allocator, n);
+            n = next;
+        }
+    }
+
+    return removed;
 }
 
 static void cx_ll_clear(struct cx_list_s *list) {
@@ -777,7 +814,7 @@
 #ifndef CX_LINKED_LIST_SWAP_SBO_SIZE
 #define CX_LINKED_LIST_SWAP_SBO_SIZE 128
 #endif
-unsigned cx_linked_list_swap_sbo_size = CX_LINKED_LIST_SWAP_SBO_SIZE;
+const unsigned cx_linked_list_swap_sbo_size = CX_LINKED_LIST_SWAP_SBO_SIZE;
 
 static int cx_ll_swap(
         struct cx_list_s *list,
@@ -798,7 +835,7 @@
         left = j;
         right = i;
     }
-    cx_linked_list_node *nleft, *nright;
+    cx_linked_list_node *nleft = NULL, *nright = NULL;
     if (left < mid && right < mid) {
         // case 1: both items left from mid
         nleft = cx_ll_node_at(ll, left);
--- a/ucx/list.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/list.c	Sun Jan 05 22:00:39 2025 +0100
@@ -59,7 +59,7 @@
 }
 
 static void cx_pl_destructor(struct cx_list_s *list) {
-    list->climpl->destructor(list);
+    list->climpl->deallocate(list);
 }
 
 static int cx_pl_insert_element(
@@ -99,11 +99,13 @@
     return list->climpl->insert_iter(iter, &elem, prepend);
 }
 
-static int cx_pl_remove(
+static size_t cx_pl_remove(
         struct cx_list_s *list,
-        size_t index
+        size_t index,
+        size_t num,
+        void *targetbuf
 ) {
-    return list->climpl->remove(list, index);
+    return list->climpl->remove(list, index, num, targetbuf);
 }
 
 static void cx_pl_clear(struct cx_list_s *list) {
@@ -210,33 +212,33 @@
 
 // <editor-fold desc="empty list implementation">
 
-static void cx_emptyl_noop(__attribute__((__unused__)) CxList *list) {
+static void cx_emptyl_noop(cx_attr_unused CxList *list) {
     // this is a noop, but MUST be implemented
 }
 
 static void *cx_emptyl_at(
-        __attribute__((__unused__)) const struct cx_list_s *list,
-        __attribute__((__unused__)) size_t index
+        cx_attr_unused const struct cx_list_s *list,
+        cx_attr_unused size_t index
 ) {
     return NULL;
 }
 
 static ssize_t cx_emptyl_find_remove(
-        __attribute__((__unused__)) struct cx_list_s *list,
-        __attribute__((__unused__)) const void *elem,
-        __attribute__((__unused__)) bool remove
+        cx_attr_unused struct cx_list_s *list,
+        cx_attr_unused const void *elem,
+        cx_attr_unused bool remove
 ) {
     return -1;
 }
 
-static bool cx_emptyl_iter_valid(__attribute__((__unused__)) const void *iter) {
+static bool cx_emptyl_iter_valid(cx_attr_unused const void *iter) {
     return false;
 }
 
 static CxIterator cx_emptyl_iterator(
         const struct cx_list_s *list,
         size_t index,
-        __attribute__((__unused__)) bool backwards
+        cx_attr_unused bool backwards
 ) {
     CxIterator iter = {0};
     iter.src_handle.c = list;
@@ -295,10 +297,9 @@
     const char *src = data;
     size_t i = 0;
     for (; i < n; i++) {
-        if (0 != invoke_list_func(insert_element,
-                                  list, index + i, src + (i * elem_size))) {
-            return i;
-        }
+        if (0 != invoke_list_func(
+            insert_element, list, index + i,
+            src + (i * elem_size))) return i;
     }
     return i;
 }
@@ -343,9 +344,8 @@
 
         // insert the elements at location si
         if (ins == 1) {
-            if (0 != invoke_list_func(insert_element,
-                                      list, di, src))
-                return inserted;
+            if (0 != invoke_list_func(
+                insert_element, list, di, src)) return inserted;
         } else {
             size_t r = invoke_list_func(insert_array, list, di, src, ins);
             if (r < ins) return inserted + r;
@@ -417,10 +417,6 @@
     return 0;
 }
 
-void cxListDestroy(CxList *list) {
-    list->cl->destructor(list);
-}
-
 int cxListCompare(
         const CxList *list,
         const CxList *other
@@ -485,3 +481,8 @@
     it.base.mutating = true;
     return it;
 }
+
+void cxListFree(CxList *list) {
+    if (list == NULL) return;
+    list->cl->deallocate(list);
+}
--- a/ucx/map.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/map.c	Sun Jan 05 22:00:39 2025 +0100
@@ -31,24 +31,24 @@
 
 // <editor-fold desc="empty map implementation">
 
-static void cx_empty_map_noop(__attribute__((__unused__)) CxMap *map) {
+static void cx_empty_map_noop(cx_attr_unused CxMap *map) {
     // this is a noop, but MUST be implemented
 }
 
 static void *cx_empty_map_get(
-        __attribute__((__unused__)) const CxMap *map,
-        __attribute__((__unused__)) CxHashKey key
+        cx_attr_unused const CxMap *map,
+        cx_attr_unused CxHashKey key
 ) {
     return NULL;
 }
 
-static bool cx_empty_map_iter_valid(__attribute__((__unused__)) const void *iter) {
+static bool cx_empty_map_iter_valid(cx_attr_unused const void *iter) {
     return false;
 }
 
 static CxIterator cx_empty_map_iterator(
         const struct cx_map_s *map,
-        __attribute__((__unused__)) enum cx_map_iterator_type type
+        cx_attr_unused enum cx_map_iterator_type type
 ) {
     CxIterator iter = {0};
     iter.src_handle.c = map;
@@ -100,3 +100,8 @@
     it.base.mutating = true;
     return it;
 }
+
+void cxMapFree(CxMap *map) {
+    if (map == NULL) return;
+    map->cl->deallocate(map);
+}
--- a/ucx/mempool.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/mempool.c	Sun Jan 05 22:00:39 2025 +0100
@@ -27,8 +27,9 @@
  */
 
 #include "cx/mempool.h"
-#include "cx/utils.h"
+
 #include <string.h>
+#include <errno.h>
 
 struct cx_mempool_memory_s {
     /** The destructor. */
@@ -45,18 +46,20 @@
 
     if (pool->size >= pool->capacity) {
         size_t newcap = pool->capacity - (pool->capacity % 16) + 16;
-        struct cx_mempool_memory_s **newdata = realloc(pool->data, newcap*sizeof(struct cx_mempool_memory_s*));
-        if (newdata == NULL) {
+        size_t newmsize;
+        if (pool->capacity > newcap || cx_szmul(newcap,
+                sizeof(struct cx_mempool_memory_s*), &newmsize)) {
+            errno = EOVERFLOW;
             return NULL;
         }
+        struct cx_mempool_memory_s **newdata = realloc(pool->data, newmsize);
+        if (newdata == NULL) return NULL;
         pool->data = newdata;
         pool->capacity = newcap;
     }
 
     struct cx_mempool_memory_s *mem = malloc(sizeof(cx_destructor_func) + n);
-    if (mem == NULL) {
-        return NULL;
-    }
+    if (mem == NULL) return NULL;
 
     mem->destructor = pool->auto_destr;
     pool->data[pool->size] = mem;
@@ -72,12 +75,11 @@
 ) {
     size_t msz;
     if (cx_szmul(nelem, elsize, &msz)) {
+        errno = EOVERFLOW;
         return NULL;
     }
     void *ptr = cx_mempool_malloc(p, msz);
-    if (ptr == NULL) {
-        return NULL;
-    }
+    if (ptr == NULL) return NULL;
     memset(ptr, 0, nelem * elsize);
     return ptr;
 }
@@ -93,17 +95,15 @@
     mem = (struct cx_mempool_memory_s*)(((char *) ptr) - sizeof(cx_destructor_func));
     newm = realloc(mem, n + sizeof(cx_destructor_func));
 
-    if (newm == NULL) {
-        return NULL;
-    }
+    if (newm == NULL) return NULL;
     if (mem != newm) {
-        cx_for_n(i, pool->size) {
+        for (size_t i = 0; i < pool->size; i++) {
             if (pool->data[i] == mem) {
                 pool->data[i] = newm;
                 return ((char*)newm) + sizeof(cx_destructor_func);
             }
         }
-        abort();
+        abort(); // LCOV_EXCL_LINE
     } else {
         return ptr;
     }
@@ -113,12 +113,13 @@
         void *p,
         void *ptr
 ) {
+    if (!ptr) return;
     struct cx_mempool_s *pool = p;
 
     struct cx_mempool_memory_s *mem = (struct cx_mempool_memory_s *)
             ((char *) ptr - sizeof(cx_destructor_func));
 
-    cx_for_n(i, pool->size) {
+    for (size_t i = 0; i < pool->size; i++) {
         if (mem == pool->data[i]) {
             if (mem->destructor) {
                 mem->destructor(mem->c);
@@ -133,12 +134,13 @@
             return;
         }
     }
-    abort();
+    abort(); // LCOV_EXCL_LINE
 }
 
-void cxMempoolDestroy(CxMempool *pool) {
+void cxMempoolFree(CxMempool *pool) {
+    if (pool == NULL) return;
     struct cx_mempool_memory_s *mem;
-    cx_for_n(i, pool->size) {
+    for (size_t i = 0; i < pool->size; i++) {
         mem = pool->data[i];
         if (mem->destructor) {
             mem->destructor(mem->c);
@@ -157,6 +159,10 @@
     *(cx_destructor_func *) ((char *) ptr - sizeof(cx_destructor_func)) = func;
 }
 
+void cxMempoolRemoveDestructor(void *ptr) {
+    *(cx_destructor_func *) ((char *) ptr - sizeof(cx_destructor_func)) = NULL;
+}
+
 struct cx_mempool_foreign_mem_s {
     cx_destructor_func destr;
     void* mem;
@@ -198,35 +204,34 @@
 ) {
     size_t poolsize;
     if (cx_szmul(capacity, sizeof(struct cx_mempool_memory_s*), &poolsize)) {
+        errno = EOVERFLOW;
         return NULL;
     }
 
     struct cx_mempool_s *pool =
             malloc(sizeof(struct cx_mempool_s));
-    if (pool == NULL) {
-        return NULL;
-    }
+    if (pool == NULL) return NULL;
 
     CxAllocator *provided_allocator = malloc(sizeof(CxAllocator));
-    if (provided_allocator == NULL) {
+    if (provided_allocator == NULL) { // LCOV_EXCL_START
         free(pool);
         return NULL;
-    }
+    } // LCOV_EXCL_STOP
     provided_allocator->cl = &cx_mempool_allocator_class;
     provided_allocator->data = pool;
 
     pool->allocator = provided_allocator;
 
     pool->data = malloc(poolsize);
-    if (pool->data == NULL) {
+    if (pool->data == NULL) { // LCOV_EXCL_START
         free(provided_allocator);
         free(pool);
         return NULL;
-    }
+    } // LCOV_EXCL_STOP
 
     pool->size = 0;
     pool->capacity = capacity;
     pool->auto_destr = destr;
 
-    return (CxMempool *) pool;
+    return pool;
 }
--- a/ucx/printf.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/printf.c	Sun Jan 05 22:00:39 2025 +0100
@@ -34,7 +34,7 @@
 #ifndef CX_PRINTF_SBO_SIZE
 #define CX_PRINTF_SBO_SIZE 512
 #endif
-unsigned const cx_printf_sbo_size = CX_PRINTF_SBO_SIZE;
+const unsigned cx_printf_sbo_size = CX_PRINTF_SBO_SIZE;
 
 int cx_fprintf(
         void *stream,
@@ -69,10 +69,10 @@
     } else {
         int len = ret + 1;
         char *newbuf = malloc(len);
-        if (!newbuf) {
+        if (!newbuf) { // LCOV_EXCL_START
             va_end(ap2);
             return -1;
-        }
+        } // LCOV_EXCL_STOP
 
         ret = vsnprintf(newbuf, len, fmt, ap2);
         va_end(ap2);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ucx/properties.c	Sun Jan 05 22:00:39 2025 +0100
@@ -0,0 +1,406 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/properties.h"
+
+#include <assert.h>
+
+const CxPropertiesConfig cx_properties_config_default = {
+        '=',
+        //'\\',
+        '#',
+        '\0',
+        '\0'
+};
+
+void cxPropertiesInit(
+        CxProperties *prop,
+        CxPropertiesConfig config
+) {
+    memset(prop, 0, sizeof(CxProperties));
+    prop->config = config;
+}
+
+void cxPropertiesDestroy(CxProperties *prop) {
+    cxBufferDestroy(&prop->input);
+    cxBufferDestroy(&prop->buffer);
+}
+
+int cxPropertiesFilln(
+        CxProperties *prop,
+        const char *buf,
+        size_t len
+) {
+    if (cxBufferEof(&prop->input)) {
+        // destroy a possible previously initialized buffer
+        cxBufferDestroy(&prop->input);
+        cxBufferInit(&prop->input, (void*) buf, len,
+            NULL, CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_AUTO_EXTEND);
+        prop->input.size = len;
+    } else {
+        if (cxBufferAppend(buf, 1, len, &prop->input) < len) return -1;
+    }
+    return 0;
+}
+
+void cxPropertiesUseStack(
+        CxProperties *prop,
+        char *buf,
+        size_t capacity
+) {
+    cxBufferInit(&prop->buffer, buf, capacity, NULL, CX_BUFFER_COPY_ON_EXTEND);
+}
+
+CxPropertiesStatus cxPropertiesNext(
+        CxProperties *prop,
+        cxstring *key,
+        cxstring *value
+) {
+    // check if we have a text buffer
+    if (prop->input.space == NULL) {
+        return CX_PROPERTIES_NULL_INPUT;
+    }
+
+    // a pointer to the buffer we want to read from
+    CxBuffer *current_buffer = &prop->input;
+
+    // check if we have rescued data
+    if (!cxBufferEof(&prop->buffer)) {
+        // check if we can now get a complete line
+        cxstring input = cx_strn(prop->input.space + prop->input.pos,
+            prop->input.size - prop->input.pos);
+        cxstring nl = cx_strchr(input, '\n');
+        if (nl.length > 0) {
+            // we add as much data to the rescue buffer as we need
+            // to complete the line
+            size_t len_until_nl = (size_t)(nl.ptr - input.ptr) + 1;
+
+            if (cxBufferAppend(input.ptr, 1,
+                len_until_nl, &prop->buffer) < len_until_nl) {
+                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+            }
+
+            // advance the position in the input buffer
+            prop->input.pos += len_until_nl;
+
+            // we now want to read from the rescue buffer
+            current_buffer = &prop->buffer;
+        } else {
+            // still not enough data, copy input buffer to internal buffer
+            if (cxBufferAppend(input.ptr, 1,
+                input.length, &prop->buffer) < input.length) {
+                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+            }
+            // reset the input buffer (make way for a re-fill)
+            cxBufferReset(&prop->input);
+            return CX_PROPERTIES_INCOMPLETE_DATA;
+        }
+    }
+
+    char comment1 = prop->config.comment1;
+    char comment2 = prop->config.comment2;
+    char comment3 = prop->config.comment3;
+    char delimiter = prop->config.delimiter;
+
+    // get one line and parse it
+    while (!cxBufferEof(current_buffer)) {
+        const char *buf = current_buffer->space + current_buffer->pos;
+        size_t len = current_buffer->size - current_buffer->pos;
+
+        /*
+         * First we check if we have at least one line. We also get indices of
+         * delimiter and comment chars
+         */
+        size_t delimiter_index = 0;
+        size_t comment_index = 0;
+        bool has_comment = false;
+
+        size_t i = 0;
+        char c = 0;
+        for (; i < len; i++) {
+            c = buf[i];
+            if (c == comment1 || c == comment2 || c == comment3) {
+                if (comment_index == 0) {
+                    comment_index = i;
+                    has_comment = true;
+                }
+            } else if (c == delimiter) {
+                if (delimiter_index == 0 && !has_comment) {
+                    delimiter_index = i;
+                }
+            } else if (c == '\n') {
+                break;
+            }
+        }
+
+        if (c != '\n') {
+            // we don't have enough data for a line, use the rescue buffer
+            assert(current_buffer != &prop->buffer);
+            // make sure that the rescue buffer does not already contain something
+            assert(cxBufferEof(&prop->buffer));
+            if (prop->buffer.space == NULL) {
+                // initialize a rescue buffer, if the user did not provide one
+                cxBufferInit(&prop->buffer, NULL, 256, NULL, CX_BUFFER_AUTO_EXTEND);
+            } else {
+                // from a previous rescue there might be already read data
+                // reset the buffer to avoid unnecessary buffer extension
+                cxBufferReset(&prop->buffer);
+            }
+            if (cxBufferAppend(buf, 1, len, &prop->buffer) < len) {
+                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+            }
+            // reset the input buffer (make way for a re-fill)
+            cxBufferReset(&prop->input);
+            return CX_PROPERTIES_INCOMPLETE_DATA;
+        }
+
+        cxstring line = has_comment ?
+                        cx_strn(buf, comment_index) :
+                        cx_strn(buf, i);
+        // check line
+        if (delimiter_index == 0) {
+            // if line is not blank ...
+            line = cx_strtrim(line);
+            // ... either no delimiter found, or key is empty
+            if (line.length > 0) {
+                if (line.ptr[0] == delimiter) {
+                    return CX_PROPERTIES_INVALID_EMPTY_KEY;
+                } else {
+                    return CX_PROPERTIES_INVALID_MISSING_DELIMITER;
+                }
+            } else {
+                // skip blank line
+                // if it was the rescue buffer, return to the original buffer
+                if (current_buffer == &prop->buffer) {
+                    // assert that the rescue buffer really does not contain more data
+                    assert(current_buffer->pos + i + 1 == current_buffer->size);
+                    // reset the rescue buffer, but don't destroy it!
+                    cxBufferReset(&prop->buffer);
+                    // continue with the input buffer
+                    current_buffer = &prop->input;
+                } else {
+                    // if it was the input buffer already, just advance the position
+                    current_buffer->pos += i + 1;
+                }
+                continue;
+            }
+        } else {
+            cxstring k = cx_strn(buf, delimiter_index);
+            cxstring val = cx_strn(
+                    buf + delimiter_index + 1,
+                    line.length - delimiter_index - 1);
+            k = cx_strtrim(k);
+            val = cx_strtrim(val);
+            if (k.length > 0) {
+                *key = k;
+                *value = val;
+                current_buffer->pos += i + 1;
+                assert(current_buffer->pos <= current_buffer->size);
+                return CX_PROPERTIES_NO_ERROR;
+            } else {
+                return CX_PROPERTIES_INVALID_EMPTY_KEY;
+            }
+        }
+        // unreachable - either we returned or skipped a blank line
+        assert(false);
+    }
+
+    // when we come to this point, all data must have been read
+    assert(cxBufferEof(&prop->buffer));
+    assert(cxBufferEof(&prop->input));
+
+    return CX_PROPERTIES_NO_DATA;
+}
+
+static int cx_properties_sink_map(
+        cx_attr_unused CxProperties *prop,
+        CxPropertiesSink *sink,
+        cxstring key,
+        cxstring value
+) {
+    CxMap *map = sink->sink;
+    CxAllocator *alloc = sink->data;
+    cxmutstr v = cx_strdup_a(alloc, value);
+    int r = cx_map_put_cxstr(map, key, v.ptr);
+    if (r != 0) cx_strfree_a(alloc, &v);
+    return r;
+}
+
+CxPropertiesSink cxPropertiesMapSink(CxMap *map) {
+    CxPropertiesSink sink;
+    sink.sink = map;
+    sink.data = cxDefaultAllocator;
+    sink.sink_func = cx_properties_sink_map;
+    return sink;
+}
+
+static int cx_properties_read_string(
+        CxProperties *prop,
+        CxPropertiesSource *src,
+        cxstring *target
+) {
+    if (prop->input.space == src->src) {
+        // when the input buffer already contains the string
+        // we have nothing more to provide
+        target->length = 0;
+    } else {
+        target->ptr = src->src;
+        target->length = src->data_size;
+    }
+    return 0;
+}
+
+static int cx_properties_read_file(
+        cx_attr_unused CxProperties *prop,
+        CxPropertiesSource *src,
+        cxstring *target
+) {
+    target->ptr = src->data_ptr;
+    target->length = fread(src->data_ptr, 1, src->data_size, src->src);
+    return ferror(src->src);
+}
+
+static int cx_properties_read_init_file(
+        cx_attr_unused CxProperties *prop,
+        CxPropertiesSource *src
+) {
+    src->data_ptr = malloc(src->data_size);
+    if (src->data_ptr == NULL) return 1;
+    return 0;
+}
+
+static void cx_properties_read_clean_file(
+        cx_attr_unused CxProperties *prop,
+        CxPropertiesSource *src
+) {
+    free(src->data_ptr);
+}
+
+CxPropertiesSource cxPropertiesStringSource(cxstring str) {
+    CxPropertiesSource src;
+    src.src = (void*) str.ptr;
+    src.data_size = str.length;
+    src.data_ptr = NULL;
+    src.read_func = cx_properties_read_string;
+    src.read_init_func = NULL;
+    src.read_clean_func = NULL;
+    return src;
+}
+
+CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len) {
+    CxPropertiesSource src;
+    src.src = (void*) str;
+    src.data_size = len;
+    src.data_ptr = NULL;
+    src.read_func = cx_properties_read_string;
+    src.read_init_func = NULL;
+    src.read_clean_func = NULL;
+    return src;
+}
+
+CxPropertiesSource cxPropertiesCstrSource(const char *str) {
+    CxPropertiesSource src;
+    src.src = (void*) str;
+    src.data_size = strlen(str);
+    src.data_ptr = NULL;
+    src.read_func = cx_properties_read_string;
+    src.read_init_func = NULL;
+    src.read_clean_func = NULL;
+    return src;
+}
+
+CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size) {
+    CxPropertiesSource src;
+    src.src = file;
+    src.data_size = chunk_size;
+    src.data_ptr = NULL;
+    src.read_func = cx_properties_read_file;
+    src.read_init_func = cx_properties_read_init_file;
+    src.read_clean_func = cx_properties_read_clean_file;
+    return src;
+}
+
+CxPropertiesStatus cxPropertiesLoad(
+        CxProperties *prop,
+        CxPropertiesSink sink,
+        CxPropertiesSource source
+) {
+    assert(source.read_func != NULL);
+    assert(sink.sink_func != NULL);
+
+    // initialize reader
+    if (source.read_init_func != NULL) {
+        if (source.read_init_func(prop, &source)) {
+            return CX_PROPERTIES_READ_INIT_FAILED;
+        }
+    }
+
+    // transfer the data from the source to the sink
+    CxPropertiesStatus status;
+    bool found = false;
+    while (true) {
+        // read input
+        cxstring input;
+        if (source.read_func(prop, &source, &input)) {
+            status = CX_PROPERTIES_READ_FAILED;
+            break;
+        }
+
+        // no more data - break
+        if (input.length == 0) {
+            status = found ? CX_PROPERTIES_NO_ERROR : CX_PROPERTIES_NO_DATA;
+            break;
+        }
+
+        // set the input buffer and read the k/v-pairs
+        cxPropertiesFill(prop, input);
+
+        CxPropertiesStatus kv_status;
+        do {
+            cxstring key, value;
+            kv_status = cxPropertiesNext(prop, &key, &value);
+            if (kv_status == CX_PROPERTIES_NO_ERROR) {
+                found = true;
+                if (sink.sink_func(prop, &sink, key, value)) {
+                    kv_status = CX_PROPERTIES_SINK_FAILED;
+                }
+            }
+        } while (kv_status == CX_PROPERTIES_NO_ERROR);
+
+        if (kv_status > CX_PROPERTIES_OK) {
+            status = kv_status;
+            break;
+        }
+    }
+
+    if (source.read_clean_func != NULL) {
+        source.read_clean_func(prop, &source);
+    }
+
+    return status;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ucx/streams.c	Sun Jan 05 22:00:39 2025 +0100
@@ -0,0 +1,93 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "cx/streams.h"
+
+#ifndef CX_STREAM_BCOPY_BUF_SIZE
+#define CX_STREAM_BCOPY_BUF_SIZE 8192
+#endif
+
+#ifndef CX_STREAM_COPY_BUF_SIZE
+#define CX_STREAM_COPY_BUF_SIZE 1024
+#endif
+
+size_t cx_stream_bncopy(
+        void *src,
+        void *dest,
+        cx_read_func rfnc,
+        cx_write_func wfnc,
+        char *buf,
+        size_t bufsize,
+        size_t n
+) {
+    if (n == 0) {
+        return 0;
+    }
+
+    char *lbuf;
+    size_t ncp = 0;
+
+    if (buf) {
+        if (bufsize == 0) return 0;
+        lbuf = buf;
+    } else {
+        if (bufsize == 0) bufsize = CX_STREAM_BCOPY_BUF_SIZE;
+        lbuf = malloc(bufsize);
+        if (lbuf == NULL) return 0;
+    }
+
+    size_t r;
+    size_t rn = bufsize > n ? n : bufsize;
+    while ((r = rfnc(lbuf, 1, rn, src)) != 0) {
+        r = wfnc(lbuf, 1, r, dest);
+        ncp += r;
+        n -= r;
+        rn = bufsize > n ? n : bufsize;
+        if (r == 0 || n == 0) {
+            break;
+        }
+    }
+
+    if (lbuf != buf) {
+        free(lbuf);
+    }
+
+    return ncp;
+}
+
+size_t cx_stream_ncopy(
+        void *src,
+        void *dest,
+        cx_read_func rfnc,
+        cx_write_func wfnc,
+        size_t n
+) {
+    char buf[CX_STREAM_COPY_BUF_SIZE];
+    return cx_stream_bncopy(src, dest, rfnc, wfnc,
+                            buf, CX_STREAM_COPY_BUF_SIZE, n);
+}
--- a/ucx/string.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/string.c	Sun Jan 05 22:00:39 2025 +0100
@@ -25,19 +25,23 @@
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  * POSSIBILITY OF SUCH DAMAGE.
  */
-
+#define CX_STR_IMPLEMENTATION
 #include "cx/string.h"
-#include "cx/utils.h"
 
 #include <string.h>
 #include <stdarg.h>
 #include <ctype.h>
-
-#ifndef _WIN32
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <float.h>
 
-#include <strings.h> // for strncasecmp()
-
-#endif // _WIN32
+#ifdef _WIN32
+#define cx_strcasecmp_impl _strnicmp
+#else
+#include <strings.h>
+#define cx_strcasecmp_impl strncasecmp
+#endif
 
 cxmutstr cx_mutstr(char *cstring) {
     return (cxmutstr) {cstring, strlen(cstring)};
@@ -61,11 +65,8 @@
     return (cxstring) {cstring, length};
 }
 
-cxstring cx_strcast(cxmutstr str) {
-    return (cxstring) {str.ptr, str.length};
-}
-
 void cx_strfree(cxmutstr *str) {
+    if (str == NULL) return;
     free(str->ptr);
     str->ptr = NULL;
     str->length = 0;
@@ -75,6 +76,7 @@
         const CxAllocator *alloc,
         cxmutstr *str
 ) {
+    if (str == NULL) return;
     cxFree(alloc, str->ptr);
     str->ptr = NULL;
     str->length = 0;
@@ -89,8 +91,9 @@
     va_list ap;
     va_start(ap, count);
     size_t size = 0;
-    cx_for_n(i, count) {
+    for (size_t i = 0; i < count; i++) {
         cxstring str = va_arg(ap, cxstring);
+        if (size > SIZE_MAX - str.length) errno = EOVERFLOW;
         size += str.length;
     }
     va_end(ap);
@@ -106,33 +109,59 @@
 ) {
     if (count == 0) return str;
 
-    cxstring *strings = calloc(count, sizeof(cxstring));
-    if (!strings) abort();
+    cxstring strings_stack[8];
+    cxstring *strings;
+    if (count > 8) {
+        strings = calloc(count, sizeof(cxstring));
+        if (strings == NULL) {
+            return (cxmutstr) {NULL, 0};
+        }
+    } else {
+        strings = strings_stack;
+    }
 
     va_list ap;
     va_start(ap, count);
 
     // get all args and overall length
+    bool overflow = false;
     size_t slen = str.length;
-    cx_for_n(i, count) {
+    for (size_t i = 0; i < count; i++) {
         cxstring s = va_arg (ap, cxstring);
         strings[i] = s;
+        if (slen > SIZE_MAX - str.length) overflow = true;
         slen += s.length;
     }
     va_end(ap);
 
+    // abort in case of overflow
+    if (overflow) {
+        errno = EOVERFLOW;
+        if (strings != strings_stack) {
+            free(strings);
+        }
+        return (cxmutstr) { NULL, 0 };
+    }
+
     // reallocate or create new string
+    char *newstr;
     if (str.ptr == NULL) {
-        str.ptr = cxMalloc(alloc, slen + 1);
+        newstr = cxMalloc(alloc, slen + 1);
     } else {
-        str.ptr = cxRealloc(alloc, str.ptr, slen + 1);
+        newstr = cxRealloc(alloc, str.ptr, slen + 1);
     }
-    if (str.ptr == NULL) abort();
+    if (newstr == NULL) {
+        if (strings != strings_stack) {
+            free(strings);
+        }
+        return (cxmutstr) {NULL, 0};
+    }
+    str.ptr = newstr;
 
     // concatenate strings
     size_t pos = str.length;
     str.length = slen;
-    cx_for_n(i, count) {
+    for (size_t i = 0; i < count; i++) {
         cxstring s = strings[i];
         memcpy(str.ptr + pos, s.ptr, s.length);
         pos += s.length;
@@ -142,7 +171,9 @@
     str.ptr[str.length] = '\0';
 
     // free temporary array
-    free(strings);
+    if (strings != strings_stack) {
+        free(strings);
+    }
 
     return str;
 }
@@ -193,7 +224,7 @@
 ) {
     chr = 0xFF & chr;
     // TODO: improve by comparing multiple bytes at once
-    cx_for_n(i, string.length) {
+    for (size_t i = 0; i < string.length; i++) {
         if (string.ptr[i] == chr) {
             return cx_strsubs(string, i);
         }
@@ -236,7 +267,7 @@
 #ifndef CX_STRSTR_SBO_SIZE
 #define CX_STRSTR_SBO_SIZE 512
 #endif
-unsigned const cx_strstr_sbo_size = CX_STRSTR_SBO_SIZE;
+const unsigned cx_strstr_sbo_size = CX_STRSTR_SBO_SIZE;
 
 cxstring cx_strstr(
         cxstring haystack,
@@ -434,10 +465,14 @@
         cxstring s2
 ) {
     if (s1.length == s2.length) {
-        return memcmp(s1.ptr, s2.ptr, s1.length);
+        return strncmp(s1.ptr, s2.ptr, s1.length);
     } else if (s1.length > s2.length) {
+        int r = strncmp(s1.ptr, s2.ptr, s2.length);
+        if (r != 0) return r;
         return 1;
     } else {
+        int r = strncmp(s1.ptr, s2.ptr, s1.length);
+        if (r != 0) return r;
         return -1;
     }
 }
@@ -447,14 +482,14 @@
         cxstring s2
 ) {
     if (s1.length == s2.length) {
-#ifdef _WIN32
-        return _strnicmp(s1.ptr, s2.ptr, s1.length);
-#else
-        return strncasecmp(s1.ptr, s2.ptr, s1.length);
-#endif
+        return cx_strcasecmp_impl(s1.ptr, s2.ptr, s1.length);
     } else if (s1.length > s2.length) {
+        int r = cx_strcasecmp_impl(s1.ptr, s2.ptr, s2.length);
+        if (r != 0) return r;
         return 1;
     } else {
+        int r = cx_strcasecmp_impl(s1.ptr, s2.ptr, s1.length);
+        if (r != 0) return r;
         return -1;
     }
 }
@@ -556,13 +591,13 @@
 }
 
 void cx_strlower(cxmutstr string) {
-    cx_for_n(i, string.length) {
+    for (size_t i = 0; i < string.length; i++) {
         string.ptr[i] = (char) tolower(string.ptr[i]);
     }
 }
 
 void cx_strupper(cxmutstr string) {
-    cx_for_n(i, string.length) {
+    for (size_t i = 0; i < string.length; i++) {
         string.ptr[i] = (char) toupper(string.ptr[i]);
     }
 }
@@ -748,7 +783,7 @@
 
     // if more delimiters are specified, check them now
     if (ctx->delim_more_count > 0) {
-        cx_for_n(i, ctx->delim_more_count) {
+        for (size_t i = 0; i < ctx->delim_more_count; i++) {
             cxstring d = cx_strstr(haystack, ctx->delim_more[i]);
             if (d.length > 0 && (delim.length == 0 || d.ptr < delim.ptr)) {
                 delim.ptr = d.ptr;
@@ -784,3 +819,370 @@
     ctx->delim_more = delim;
     ctx->delim_more_count = count;
 }
+
+#define cx_strtoX_signed_impl(rtype, rmin, rmax) \
+    long long result; \
+    if (cx_strtoll_lc(str, &result, base, groupsep)) { \
+        return -1; \
+    } \
+    if (result < rmin || result > rmax) { \
+        errno = ERANGE; \
+        return -1; \
+    } \
+    *output = (rtype) result; \
+    return 0
+
+int cx_strtos_lc(cxstring str, short *output, int base, const char *groupsep) {
+    cx_strtoX_signed_impl(short, SHRT_MIN, SHRT_MAX);
+}
+
+int cx_strtoi_lc(cxstring str, int *output, int base, const char *groupsep) {
+    cx_strtoX_signed_impl(int, INT_MIN, INT_MAX);
+}
+
+int cx_strtol_lc(cxstring str, long *output, int base, const char *groupsep) {
+    cx_strtoX_signed_impl(long, LONG_MIN, LONG_MAX);
+}
+
+int cx_strtoll_lc(cxstring str, long long *output, int base, const char *groupsep) {
+    // strategy: parse as unsigned, check range, negate if required
+    bool neg = false;
+    size_t start_unsigned = 0;
+
+    // trim already, to search for a sign character
+    str = cx_strtrim(str);
+    if (str.length == 0) {
+        errno = EINVAL;
+        return -1;
+    }
+
+    // test if we have a negative sign character
+    if (str.ptr[start_unsigned] == '-') {
+        neg = true;
+        start_unsigned++;
+        // must not be followed by positive sign character
+        if (str.length == 1 || str.ptr[start_unsigned] == '+') {
+            errno = EINVAL;
+            return -1;
+        }
+    }
+
+    // now parse the number with strtoull
+    unsigned long long v;
+    cxstring ustr = start_unsigned == 0 ? str
+        : cx_strn(str.ptr + start_unsigned, str.length - start_unsigned);
+    int ret = cx_strtoull_lc(ustr, &v, base, groupsep);
+    if (ret != 0) return ret;
+    if (neg) {
+        if (v - 1 > LLONG_MAX) {
+            errno = ERANGE;
+            return -1;
+        }
+        *output = -(long long) v;
+        return 0;
+    } else {
+        if (v > LLONG_MAX) {
+            errno = ERANGE;
+            return -1;
+        }
+        *output = (long long) v;
+        return 0;
+    }
+}
+
+int cx_strtoi8_lc(cxstring str, int8_t *output, int base, const char *groupsep) {
+    cx_strtoX_signed_impl(int8_t, INT8_MIN, INT8_MAX);
+}
+
+int cx_strtoi16_lc(cxstring str, int16_t *output, int base, const char *groupsep) {
+    cx_strtoX_signed_impl(int16_t, INT16_MIN, INT16_MAX);
+}
+
+int cx_strtoi32_lc(cxstring str, int32_t *output, int base, const char *groupsep) {
+    cx_strtoX_signed_impl(int32_t, INT32_MIN, INT32_MAX);
+}
+
+int cx_strtoi64_lc(cxstring str, int64_t *output, int base, const char *groupsep) {
+    assert(sizeof(long long) == sizeof(int64_t)); // should be true on all platforms
+    return cx_strtoll_lc(str, (long long*) output, base, groupsep);
+}
+
+int cx_strtoz_lc(cxstring str, ssize_t *output, int base, const char *groupsep) {
+#if SSIZE_MAX == INT32_MAX
+    return cx_strtoi32_lc(str, (int32_t*) output, base, groupsep);
+#elif SSIZE_MAX == INT64_MAX
+    return cx_strtoll_lc(str, (long long*) output, base, groupsep);
+#else
+#error "unsupported ssize_t size"
+#endif
+}
+
+#define cx_strtoX_unsigned_impl(rtype, rmax) \
+    uint64_t result; \
+    if (cx_strtou64_lc(str, &result, base, groupsep)) { \
+        return -1; \
+    } \
+    if (result > rmax) { \
+        errno = ERANGE; \
+        return -1; \
+    } \
+    *output = (rtype) result; \
+    return 0
+
+int cx_strtous_lc(cxstring str, unsigned short *output, int base, const char *groupsep) {
+    cx_strtoX_unsigned_impl(unsigned short, USHRT_MAX);
+}
+
+int cx_strtou_lc(cxstring str, unsigned int *output, int base, const char *groupsep) {
+    cx_strtoX_unsigned_impl(unsigned int, UINT_MAX);
+}
+
+int cx_strtoul_lc(cxstring str, unsigned long *output, int base, const char *groupsep) {
+    cx_strtoX_unsigned_impl(unsigned long, ULONG_MAX);
+}
+
+int cx_strtoull_lc(cxstring str, unsigned long long *output, int base, const char *groupsep) {
+    // some sanity checks
+    str = cx_strtrim(str);
+    if (str.length == 0) {
+        errno = EINVAL;
+        return -1;
+    }
+    if (!(base == 2 || base == 8 || base == 10 || base == 16)) {
+        errno = EINVAL;
+        return -1;
+    }
+    if (groupsep == NULL) groupsep = "";
+
+    // find the actual start of the number
+    if (str.ptr[0] == '+') {
+        str.ptr++;
+        str.length--;
+        if (str.length == 0) {
+            errno = EINVAL;
+            return -1;
+        }
+    }
+    size_t start = 0;
+
+    // if base is 2 or 16, some leading stuff may appear
+    if (base == 2) {
+        if ((str.ptr[0] | 32) == 'b') {
+            start = 1;
+        } else if (str.ptr[0] == '0' && str.length > 1) {
+            if ((str.ptr[1] | 32) == 'b') {
+                start = 2;
+            }
+        }
+    } else if (base == 16) {
+        if ((str.ptr[0] | 32) == 'x' || str.ptr[0] == '#') {
+            start = 1;
+        } else if (str.ptr[0] == '0' && str.length > 1) {
+            if ((str.ptr[1] | 32) == 'x') {
+                start = 2;
+            }
+        }
+    }
+
+    // check if there are digits left
+    if (start >= str.length) {
+        errno = EINVAL;
+        return -1;
+    }
+
+    // now parse the number
+    unsigned long long result = 0;
+    for (size_t i = start; i < str.length; i++) {
+        // ignore group separators
+        if (strchr(groupsep, str.ptr[i])) continue;
+
+        // determine the digit value of the character
+        unsigned char c = str.ptr[i];
+        if (c >= 'a') c = 10 + (c - 'a');
+        else if (c >= 'A') c = 10 + (c - 'A');
+        else if (c >= '0') c = c - '0';
+        else c = 255;
+        if (c >= base) {
+            errno = EINVAL;
+            return -1;
+        }
+
+        // now combine the digit with what we already have
+        unsigned long right = (result & 0xff) * base + c;
+        unsigned long long left = (result >> 8) * base + (right >> 8);
+        if (left > (ULLONG_MAX >> 8)) {
+            errno = ERANGE;
+            return -1;
+        }
+        result = (left << 8) + (right & 0xff);
+    }
+
+    *output = result;
+    return 0;
+}
+
+int cx_strtou8_lc(cxstring str, uint8_t *output, int base, const char *groupsep) {
+    cx_strtoX_unsigned_impl(uint8_t, UINT8_MAX);
+}
+
+int cx_strtou16_lc(cxstring str, uint16_t *output, int base, const char *groupsep) {
+    cx_strtoX_unsigned_impl(uint16_t, UINT16_MAX);
+}
+
+int cx_strtou32_lc(cxstring str, uint32_t *output, int base, const char *groupsep) {
+    cx_strtoX_unsigned_impl(uint32_t, UINT32_MAX);
+}
+
+int cx_strtou64_lc(cxstring str, uint64_t *output, int base, const char *groupsep) {
+    assert(sizeof(unsigned long long) == sizeof(uint64_t)); // should be true on all platforms
+    return cx_strtoull_lc(str, (unsigned long long*) output, base, groupsep);
+}
+
+int cx_strtouz_lc(cxstring str, size_t *output, int base, const char *groupsep) {
+#if SIZE_MAX == UINT32_MAX
+    return cx_strtou32_lc(str, (uint32_t*) output, base, groupsep);
+#elif SIZE_MAX == UINT64_MAX
+    return cx_strtoull_lc(str, (unsigned long long *) output, base, groupsep);
+#else
+#error "unsupported size_t size"
+#endif
+}
+
+int cx_strtof_lc(cxstring str, float *output, char decsep, const char *groupsep) {
+    // use string to double and add a range check
+    double d;
+    int ret = cx_strtod_lc(str, &d, decsep, groupsep);
+    if (ret != 0) return ret;
+    // note: FLT_MIN is the smallest POSITIVE number that can be represented
+    double test = d < 0 ? -d : d;
+    if (test < FLT_MIN || test > FLT_MAX) {
+        errno = ERANGE;
+        return -1;
+    }
+    *output = (float) d;
+    return 0;
+}
+
+int cx_strtod_lc(cxstring str, double *output, char decsep, const char *groupsep) {
+    // TODO: overflow check
+    // TODO: increase precision
+
+    // trim and check
+    str = cx_strtrim(str);
+    if (str.length == 0) {
+        errno = EINVAL;
+        return -1;
+    }
+
+    double result = 0.;
+    int sign = 1;
+
+    // check if there is a sign
+    if (str.ptr[0] == '-') {
+        sign = -1;
+        str.ptr++;
+        str.length--;
+    } else if (str.ptr[0] == '+') {
+        str.ptr++;
+        str.length--;
+    }
+
+    // there must be at least one char to parse
+    if (str.length == 0) {
+        errno = EINVAL;
+        return -1;
+    }
+
+    // parse all digits until we find the decsep
+    size_t pos = 0;
+    do {
+        if (isdigit(str.ptr[pos])) {
+            result = result * 10 + (str.ptr[pos] - '0');
+        } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
+            break;
+        }
+    } while (++pos < str.length);
+
+    // already done?
+    if (pos == str.length) {
+        *output = result * sign;
+        return 0;
+    }
+
+    // is the next char the decsep?
+    if (str.ptr[pos] == decsep) {
+        pos++;
+        // it may end with the decsep, if it did not start with it
+        if (pos == str.length) {
+            if (str.length == 1) {
+                errno = EINVAL;
+                return -1;
+            } else {
+                *output = result * sign;
+                return 0;
+            }
+        }
+        // parse everything until exponent or end
+        double factor = 1.;
+        do {
+            if (isdigit(str.ptr[pos])) {
+                factor *= 0.1;
+                result = result + factor * (str.ptr[pos] - '0');
+            } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
+                break;
+            }
+        } while (++pos < str.length);
+    }
+
+    // no exponent?
+    if (pos == str.length) {
+        *output = result * sign;
+        return 0;
+    }
+
+    // now the next separator MUST be the exponent separator
+    // and at least one char must follow
+    if ((str.ptr[pos] | 32) != 'e' || str.length <= pos + 1) {
+        errno = EINVAL;
+        return -1;
+    }
+    pos++;
+
+    // check if we have a sign for the exponent
+    double factor = 10.;
+    if (str.ptr[pos] == '-') {
+        factor = .1;
+        pos++;
+    } else if (str.ptr[pos] == '+') {
+        pos++;
+    }
+
+    // at least one digit must follow
+    if (pos == str.length) {
+        errno = EINVAL;
+        return -1;
+    }
+
+    // parse the exponent
+    unsigned int exp = 0;
+    do {
+        if (isdigit(str.ptr[pos])) {
+            exp = 10 * exp + (str.ptr[pos] - '0');
+        } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
+            errno = EINVAL;
+            return -1;
+        }
+    } while (++pos < str.length);
+
+    // apply the exponent by fast exponentiation
+    do {
+        if (exp & 1) {
+            result *= factor;
+        }
+        factor *= factor;
+    } while ((exp >>= 1) > 0);
+
+    // store the result and exit
+    *output = result * sign;
+    return 0;
+}
--- a/ucx/szmul.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/szmul.c	Sun Jan 05 22:00:39 2025 +0100
@@ -26,6 +26,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
+#include "cx/common.h"
+
+#ifndef CX_SZMUL_BUILTIN
 int cx_szmul_impl(
         size_t a,
         size_t b,
@@ -44,3 +47,4 @@
         return 1;
     }
 }
+#endif // CX_SZMUL_BUILTIN
--- a/ucx/tree.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ucx/tree.c	Sun Jan 05 22:00:39 2025 +0100
@@ -42,6 +42,13 @@
 #define cx_tree_ptr_locations \
     loc_parent, loc_children, loc_last_child, loc_prev, loc_next
 
+#define cx_tree_node_layout(tree) \
+    (tree)->loc_parent,\
+    (tree)->loc_children,\
+    (tree)->loc_last_child,\
+    (tree)->loc_prev,  \
+    (tree)->loc_next
+
 static void cx_tree_zero_pointers(
         void *node,
         ptrdiff_t loc_parent,
@@ -51,7 +58,9 @@
         ptrdiff_t loc_next
 ) {
     tree_parent(node) = NULL;
-    tree_prev(node) = NULL;
+    if (loc_prev >= 0) {
+        tree_prev(node) = NULL;
+    }
     tree_next(node) = NULL;
     tree_children(node) = NULL;
     if (loc_last_child >= 0) {
@@ -60,14 +69,18 @@
 }
 
 void cx_tree_link(
-        void *restrict parent,
-        void *restrict node,
+        void *parent,
+        void *node,
         ptrdiff_t loc_parent,
         ptrdiff_t loc_children,
         ptrdiff_t loc_last_child,
         ptrdiff_t loc_prev,
         ptrdiff_t loc_next
 ) {
+    assert(loc_parent >= 0);
+    assert(loc_children >= 0);
+    assert(loc_next >= 0);
+
     void *current_parent = tree_parent(node);
     if (current_parent == parent) return;
     if (current_parent != NULL) {
@@ -80,24 +93,43 @@
             tree_last_child(parent) = node;
         }
     } else {
+        void *child;
         if (loc_last_child >= 0) {
-            void *child = tree_last_child(parent);
-            tree_prev(node) = child;
-            tree_next(child) = node;
+            child = tree_last_child(parent);
             tree_last_child(parent) = node;
         } else {
-            void *child = tree_children(parent);
+            child = tree_children(parent);
             void *next;
             while ((next = tree_next(child)) != NULL) {
                 child = next;
             }
+        }
+        if (loc_prev >= 0) {
             tree_prev(node) = child;
-            tree_next(child) = node;
         }
+        tree_next(child) = node;
     }
     tree_parent(node) = parent;
 }
 
+static void *cx_tree_node_prev(
+        ptrdiff_t loc_parent,
+        ptrdiff_t loc_children,
+        ptrdiff_t loc_next,
+        const void *node
+) {
+    void *parent = tree_parent(node);
+    void *begin = tree_children(parent);
+    if (begin == node) return NULL;
+    const void *cur = begin;
+    const void *next;
+    while (1) {
+        next = tree_next(cur);
+        if (next == node) return (void *) cur;
+        cur = next;
+    }
+}
+
 void cx_tree_unlink(
         void *node,
         ptrdiff_t loc_parent,
@@ -108,7 +140,15 @@
 ) {
     if (tree_parent(node) == NULL) return;
 
-    void *left = tree_prev(node);
+    assert(loc_children >= 0);
+    assert(loc_next >= 0);
+    assert(loc_parent >= 0);
+    void *left;
+    if (loc_prev >= 0) {
+        left = tree_prev(node);
+    } else {
+        left = cx_tree_node_prev(loc_parent, loc_children, loc_next, node);
+    }
     void *right = tree_next(node);
     void *parent = tree_parent(node);
     assert(left == NULL || tree_children(parent) != node);
@@ -125,94 +165,95 @@
             tree_last_child(parent) = left;
         }
     } else {
-        tree_prev(right) = left;
+        if (loc_prev >= 0) {
+            tree_prev(right) = left;
+        }
     }
 
     tree_parent(node) = NULL;
-    tree_prev(node) = NULL;
     tree_next(node) = NULL;
+    if (loc_prev >= 0) {
+        tree_prev(node) = NULL;
+    }
 }
 
 int cx_tree_search(
         const void *root,
+        size_t depth,
         const void *node,
         cx_tree_search_func sfunc,
         void **result,
         ptrdiff_t loc_children,
         ptrdiff_t loc_next
 ) {
-    int ret;
+    // help avoiding bugs due to uninitialized memory
+    assert(result != NULL);
     *result = NULL;
 
-    // shortcut: compare root before doing anything else
-    ret = sfunc(root, node);
+    // remember return value for best match
+    int ret = sfunc(root, node);
     if (ret < 0) {
-        return ret;
-    } else if (ret == 0 || tree_children(root) == NULL) {
-        *result = (void*)root;
+        // not contained, exit
+        return -1;
+    }
+    *result = (void*) root;
+    // if root is already exact match, exit
+    if (ret == 0) {
+        return 0;
+    }
+
+    // when depth is one, we are already done
+    if (depth == 1) {
         return ret;
     }
 
-    // create a working stack
-    CX_ARRAY_DECLARE(const void *, work);
-    cx_array_initialize(work, 32);
+    // special case: indefinite depth
+    if (depth == 0) {
+        depth = SIZE_MAX;
+    }
+
+    // create an iterator
+    CxTreeIterator iter = cx_tree_iterator(
+            (void*) root, false, loc_children, loc_next
+    );
+
+    // skip root, we already handled it
+    cxIteratorNext(iter);
 
-    // add the children of root to the working stack
-    {
-        void *c = tree_children(root);
-        while (c != NULL) {
-            cx_array_simple_add(work, c);
-            c = tree_next(c);
+    // loop through the remaining tree
+    cx_foreach(void *, elem, iter) {
+        // investigate the current node
+        int ret_elem = sfunc(elem, node);
+        if (ret_elem == 0) {
+            // if found, exit the search
+            *result = (void *) elem;
+            ret = 0;
+            break;
+        } else if (ret_elem > 0 && ret_elem < ret) {
+            // new distance is better
+            *result = elem;
+            ret = ret_elem;
+        } else {
+            // not contained or distance is worse, skip entire subtree
+            cxTreeIteratorContinue(iter);
+        }
+
+        // when we reached the max depth, skip the subtree
+        if (iter.depth == depth) {
+            cxTreeIteratorContinue(iter);
         }
     }
 
-    // remember a candidate for adding the data
-    // also remember the exact return code from sfunc
-    void *candidate = (void *) root;
-    int ret_candidate = ret;
-
-    // process the working stack
-    while (work_size > 0) {
-        // pop element
-        const void *elem = work[--work_size];
-
-        // apply the search function
-        ret = sfunc(elem, node);
+    // dispose the iterator as we might have exited the loop early
+    cxTreeIteratorDispose(&iter);
 
-        if (ret == 0) {
-            // if found, exit the search
-            *result = (void *) elem;
-            work_size = 0;
-            break;
-        } else if (ret > 0) {
-            // if children might contain the data, add them to the stack
-            void *c = tree_children(elem);
-            while (c != NULL) {
-                cx_array_simple_add(work, c);
-                c = tree_next(c);
-            }
-
-            // remember this node in case no child is suitable
-            if (ret < ret_candidate) {
-                candidate = (void *) elem;
-                ret_candidate = ret;
-            }
-        }
-    }
-
-    // not found, but was there a candidate?
-    if (ret != 0 && candidate != NULL) {
-        ret = ret_candidate;
-        *result = candidate;
-    }
-
-    // free the working queue and return
-    free(work);
+    assert(ret < 0 || *result != NULL);
     return ret;
 }
 
 int cx_tree_search_data(
         const void *root,
+        size_t depth,
         const void *data,
         cx_tree_search_data_func sfunc,
         void **result,
@@ -221,7 +262,7 @@
 ) {
     // it is basically the same implementation
     return cx_tree_search(
-            root, data,
+            root, depth, data,
             (cx_tree_search_func) sfunc,
             result,
             loc_children, loc_next);
@@ -369,7 +410,7 @@
     return iter->node;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static void cx_tree_visitor_enqueue_siblings(
         struct cx_tree_visitor_s *iter, void *node, ptrdiff_t loc_next) {
     node = tree_next(node);
@@ -531,6 +572,7 @@
     void *match = NULL;
     int result = cx_tree_search(
             root,
+            0,
             *cnode,
             sfunc,
             &match,
@@ -595,6 +637,7 @@
         cx_tree_add_look_around_retry:
         result = cx_tree_search(
                 current_node,
+                0,
                 new_node,
                 sfunc,
                 &match,
@@ -681,37 +724,6 @@
                             loc_prev, loc_next);
 }
 
-static void cx_tree_default_destructor(CxTree *tree) {
-    if (tree->simple_destructor != NULL || tree->advanced_destructor != NULL) {
-        CxTreeIterator iter = tree->cl->iterator(tree, true);
-        cx_foreach(void *, node, iter) {
-            if (iter.exiting) {
-                if (tree->simple_destructor) {
-                    tree->simple_destructor(node);
-                }
-                if (tree->advanced_destructor) {
-                    tree->advanced_destructor(tree->destructor_data, node);
-                }
-            }
-        }
-    }
-    cxFree(tree->allocator, tree);
-}
-
-static CxTreeIterator cx_tree_default_iterator(
-        CxTree *tree,
-        bool visit_on_exit
-) {
-    return cx_tree_iterator(
-            tree->root, visit_on_exit,
-            tree->loc_children, tree->loc_next
-    );
-}
-
-static CxTreeVisitor cx_tree_default_visitor(CxTree *tree) {
-    return cx_tree_visitor(tree->root, tree->loc_children, tree->loc_next);
-}
-
 static int cx_tree_default_insert_element(
         CxTree *tree,
         const void *data
@@ -765,13 +777,15 @@
 static void *cx_tree_default_find(
         CxTree *tree,
         const void *subtree,
-        const void *data
+        const void *data,
+        size_t depth
 ) {
     if (tree->root == NULL) return NULL;
 
     void *found;
     if (0 == cx_tree_search_data(
             subtree,
+            depth,
             data,
             tree->search_data,
             &found,
@@ -785,12 +799,9 @@
 }
 
 static cx_tree_class cx_tree_default_class = {
-        cx_tree_default_destructor,
         cx_tree_default_insert_element,
         cx_tree_default_insert_many,
-        cx_tree_default_find,
-        cx_tree_default_iterator,
-        cx_tree_default_visitor
+        cx_tree_default_find
 };
 
 CxTree *cxTreeCreate(
@@ -804,6 +815,13 @@
         ptrdiff_t loc_prev,
         ptrdiff_t loc_next
 ) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
+    }
+    assert(create_func != NULL);
+    assert(search_func != NULL);
+    assert(search_data_func != NULL);
+
     CxTree *tree = cxMalloc(allocator, sizeof(CxTree));
     if (tree == NULL) return NULL;
 
@@ -812,6 +830,7 @@
     tree->node_create = create_func;
     tree->search = search_func;
     tree->search_data = search_data_func;
+    tree->simple_destructor = NULL;
     tree->advanced_destructor = (cx_destructor_func2) cxFree;
     tree->destructor_data = (void *) allocator;
     tree->loc_parent = loc_parent;
@@ -825,6 +844,14 @@
     return tree;
 }
 
+void cxTreeFree(CxTree *tree) {
+    if (tree == NULL) return;
+    if (tree->root != NULL) {
+        cxTreeClear(tree);
+    }
+    cxFree(tree->allocator, tree);
+}
+
 CxTree *cxTreeCreateWrapped(
         const CxAllocator *allocator,
         void *root,
@@ -834,6 +861,11 @@
         ptrdiff_t loc_prev,
         ptrdiff_t loc_next
 ) {
+    if (allocator == NULL) {
+        allocator = cxDefaultAllocator;
+    }
+    assert(root != NULL);
+
     CxTree *tree = cxMalloc(allocator, sizeof(CxTree));
     if (tree == NULL) return NULL;
 
@@ -856,6 +888,27 @@
     return tree;
 }
 
+void cxTreeSetParent(
+        CxTree *tree,
+        void *parent,
+        void *child
+) {
+    size_t loc_parent = tree->loc_parent;
+    if (tree_parent(child) == NULL) {
+        tree->size++;
+    }
+    cx_tree_link(parent, child, cx_tree_node_layout(tree));
+}
+
+void cxTreeAddChildNode(
+        CxTree *tree,
+        void *parent,
+        void *child
+) {
+    cx_tree_link(parent, child, cx_tree_node_layout(tree));
+    tree->size++;
+}
+
 int cxTreeAddChild(
         CxTree *tree,
         void *parent,
@@ -893,14 +946,16 @@
 }
 
 size_t cxTreeDepth(CxTree *tree) {
-    CxTreeVisitor visitor = tree->cl->visitor(tree);
+    CxTreeVisitor visitor = cx_tree_visitor(
+            tree->root, tree->loc_children, tree->loc_next
+    );
     while (cxIteratorValid(visitor)) {
         cxIteratorNext(visitor);
     }
     return visitor.depth;
 }
 
-int cxTreeRemove(
+int cxTreeRemoveNode(
         CxTree *tree,
         void *node,
         cx_tree_relink_func relink_func
@@ -956,3 +1011,44 @@
     cx_tree_unlink(node, cx_tree_node_layout(tree));
     tree->size -= subtree_size;
 }
+
+int cxTreeDestroyNode(
+        CxTree *tree,
+        void *node,
+        cx_tree_relink_func relink_func
+) {
+    int result = cxTreeRemoveNode(tree, node, relink_func);
+    if (result == 0) {
+        if (tree->simple_destructor) {
+            tree->simple_destructor(node);
+        }
+        if (tree->advanced_destructor) {
+            tree->advanced_destructor(tree->destructor_data, node);
+        }
+        return 0;
+    } else {
+        return result;
+    }
+}
+
+void cxTreeDestroySubtree(CxTree *tree, void *node) {
+    cx_tree_unlink(node, cx_tree_node_layout(tree));
+    CxTreeIterator iter = cx_tree_iterator(
+            node, true,
+            tree->loc_children, tree->loc_next
+    );
+    cx_foreach(void *, child, iter) {
+        if (iter.exiting) {
+            if (tree->simple_destructor) {
+                tree->simple_destructor(child);
+            }
+            if (tree->advanced_destructor) {
+                tree->advanced_destructor(tree->destructor_data, child);
+            }
+        }
+    }
+    tree->size -= iter.counter;
+    if (node == tree->root) {
+        tree->root = NULL;
+    }
+}
--- a/ucx/utils.c	Sun Jan 05 17:41:39 2025 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,99 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2021 Mike Becker, Olaf Wintermann All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-#include "cx/utils.h"
-
-#ifndef CX_STREAM_BCOPY_BUF_SIZE
-#define CX_STREAM_BCOPY_BUF_SIZE 8192
-#endif
-
-#ifndef CX_STREAM_COPY_BUF_SIZE
-#define CX_STREAM_COPY_BUF_SIZE 1024
-#endif
-
-size_t cx_stream_bncopy(
-        void *src,
-        void *dest,
-        cx_read_func rfnc,
-        cx_write_func wfnc,
-        char *buf,
-        size_t bufsize,
-        size_t n
-) {
-    if (n == 0) {
-        return 0;
-    }
-
-    char *lbuf;
-    size_t ncp = 0;
-
-    if (buf) {
-        if (bufsize == 0) return 0;
-        lbuf = buf;
-    } else {
-        if (bufsize == 0) bufsize = CX_STREAM_BCOPY_BUF_SIZE;
-        lbuf = malloc(bufsize);
-        if (lbuf == NULL) {
-            return 0;
-        }
-    }
-
-    size_t r;
-    size_t rn = bufsize > n ? n : bufsize;
-    while ((r = rfnc(lbuf, 1, rn, src)) != 0) {
-        r = wfnc(lbuf, 1, r, dest);
-        ncp += r;
-        n -= r;
-        rn = bufsize > n ? n : bufsize;
-        if (r == 0 || n == 0) {
-            break;
-        }
-    }
-
-    if (lbuf != buf) {
-        free(lbuf);
-    }
-
-    return ncp;
-}
-
-size_t cx_stream_ncopy(
-        void *src,
-        void *dest,
-        cx_read_func rfnc,
-        cx_write_func wfnc,
-        size_t n
-) {
-    char buf[CX_STREAM_COPY_BUF_SIZE];
-    return cx_stream_bncopy(src, dest, rfnc, wfnc,
-                            buf, CX_STREAM_COPY_BUF_SIZE, n);
-}
-
-#ifndef CX_SZMUL_BUILTIN
-#include "szmul.c"
-#endif
--- a/ui/common/context.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/common/context.c	Sun Jan 05 22:00:39 2025 +0100
@@ -164,7 +164,7 @@
         ctx->detach_document2(ctx, doc);
     }
     
-    cxListDestroy(ls);
+    cxListFree(ls);
 }
 
 static UiVar* ctx_getvar(UiContext *ctx, CxHashKey key) {
@@ -466,7 +466,7 @@
 }
 
 UIEXPORT void ui_context_destroy(UiContext *ctx) {
-    cxMempoolDestroy(ctx->mp);
+    cxMempoolFree(ctx->mp);
 }
 
 
@@ -535,7 +535,7 @@
     
     uic_add_group_widget(ctx, widget, enable, groups);
     
-    cxListDestroy(groups);
+    cxListFree(groups);
 }
 
 size_t uic_group_array_size(const int *groups) {
--- a/ui/common/document.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/common/document.c	Sun Jan 05 22:00:39 2025 +0100
@@ -110,7 +110,7 @@
         if(ctx->close_callback) {
             ctx->close_callback(&ev, ctx->close_data);
         }
-        cxMempoolDestroy(ctx->mp);
+        cxMempoolFree(ctx->mp);
     }
 }
 
--- a/ui/common/menu.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/common/menu.c	Sun Jan 05 22:00:39 2025 +0100
@@ -326,6 +326,6 @@
         free_menuitem(m);
         m = next;
     }
-    cxListDestroy(builder->current);
+    cxListFree(builder->current);
     free(builder);
 }
--- a/ui/common/object.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/common/object.c	Sun Jan 05 22:00:39 2025 +0100
@@ -87,7 +87,7 @@
         ev.intval = 0;
         obj->ctx->close_callback(&ev, obj->ctx->close_data);
     }
-    cxMempoolDestroy(obj->ctx->mp);
+    cxMempoolFree(obj->ctx->mp);
 }
 
 UiObject* uic_object_new(UiObject *toplevel, UIWIDGET widget) {
--- a/ui/common/types.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/common/types.c	Sun Jan 05 22:00:39 2025 +0100
@@ -117,7 +117,7 @@
 }
 
 void ui_list_free(UiList *list) {
-    cxListDestroy(list->data);
+    cxListFree(list->data);
     free(list);
 }
 
@@ -213,7 +213,7 @@
         info->titles[i] = c->name;
         i++;
     }
-    cxListDestroy(cols);
+    cxListFree(cols);
     
     return info;
 }
--- a/ui/gtk/container.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/gtk/container.c	Sun Jan 05 22:00:39 2025 +0100
@@ -971,7 +971,8 @@
     int j = 0;
     while(elm) {
         CxHashKey key = cx_hash_key(&elm, sizeof(void*));
-        UiObject *item_obj = cxMapRemoveAndGet(ct->current_items, key);
+        UiObject *item_obj = NULL;
+        cxMapRemoveAndGet(ct->current_items, key, &item_obj);
         if(item_obj) {
             g_object_ref(G_OBJECT(item_obj->widget));
             BOX_REMOVE(ct->widget, item_obj->widget);
@@ -982,7 +983,7 @@
     }
     
     // ct->current_items only contains elements, that are not in the list
-    cxMapDestroy(ct->current_items); // calls destructor remove_item
+    cxMapFree(ct->current_items); // calls destructor remove_item
     ct->current_items = new_items;
     
     // add all items
@@ -1021,7 +1022,7 @@
 
 static void destroy_itemlist_container(GtkWidget *w, UiGtkItemListContainer *container) {
     container->remove_items = FALSE;
-    cxMapDestroy(container->current_items);
+    cxMapFree(container->current_items);
     free(container);
 }
 
--- a/ui/gtk/dnd.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/gtk/dnd.c	Sun Jan 05 22:00:39 2025 +0100
@@ -191,7 +191,7 @@
 }
 
 void ui_dnd_free(UiDnD *dnd) {
-    cxListDestroy(dnd->providers);
+    cxListFree(dnd->providers);
     free(dnd);
 }
 
--- a/ui/gtk/list.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/gtk/list.c	Sun Jan 05 22:00:39 2025 +0100
@@ -1500,14 +1500,14 @@
 /* ------------------------------ Source List ------------------------------ */
 
 static void ui_destroy_sourcelist(GtkWidget *w, UiListBox *v) {
-    cxListDestroy(v->sublists);
+    cxListFree(v->sublists);
     free(v);
 }
 
 static void sublist_destroy(UiObject *obj, UiListBoxSubList *sublist) {
     free(sublist->header);
     ui_destroy_boundvar(obj->ctx, sublist->var);
-    cxListDestroy(sublist->widgets);
+    cxListFree(sublist->widgets);
 }
 
 static void listbox_create_header(GtkListBoxRow* row, GtkListBoxRow* before, gpointer user_data) {
--- a/ui/gtk/menu.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/gtk/menu.c	Sun Jan 05 22:00:39 2025 +0100
@@ -131,7 +131,7 @@
         CxList *groups = cxArrayListCreateSimple(sizeof(int), i->ngroups);
         cxListAddArray(groups, i->groups, i->ngroups);
         uic_add_group_widget(obj->ctx, widget, (ui_enablefunc)ui_set_enabled, groups);
-        cxListDestroy(groups);
+        cxListFree(groups);
     }
 }
 
@@ -464,7 +464,7 @@
         CxList *groups = cxArrayListCreateSimple(sizeof(int), i->ngroups);
         cxListAddArray(groups, i->groups, i->ngroups);
         uic_add_group_widget(obj->ctx, action, (ui_enablefunc)action_enable, groups);
-        cxListDestroy(groups);
+        cxListFree(groups);
     }
     
     if(i->callback != NULL) {
--- a/ui/motif/button.c	Sun Jan 05 17:41:39 2025 +0100
+++ b/ui/motif/button.c	Sun Jan 05 22:00:39 2025 +0100
@@ -239,7 +239,7 @@
 }
 
 static void destroy_list(Widget w, CxList *list, XtPointer d) {
-    cxListDestroy(list);
+    cxListFree(list);
 }
 
 static void radiobutton_changed(Widget w, UiVarEventData *event, XmToggleButtonCallbackStruct *tb) {

mercurial