update ucx to version 4.0 default tip

Wed, 31 Dec 2025 16:40:12 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Wed, 31 Dec 2025 16:40:12 +0100
changeset 1040
473d8cb58a6c
parent 1039
6691e007cef7

update ucx to version 4.0

ucx/allocator.c file | annotate | diff | comparison | revisions
ucx/array_list.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/kv_list.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/json.c file | annotate | diff | comparison | revisions
ucx/kv_list.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/properties.c file | annotate | diff | comparison | revisions
ucx/streams.c file | annotate | diff | comparison | revisions
ucx/string.c file | annotate | diff | comparison | revisions
ucx/tree.c file | annotate | diff | comparison | revisions
ui/motif/pathbar.c file | annotate | diff | comparison | revisions
--- a/ucx/allocator.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/allocator.c	Wed Dec 31 16:40:12 2025 +0100
@@ -61,14 +61,14 @@
 #endif
 
 static void *cx_malloc_stdlib(
-        cx_attr_unused void *d,
+        CX_UNUSED void *d,
         size_t n
 ) {
     return malloc(n);
 }
 
 static void *cx_realloc_stdlib(
-        cx_attr_unused void *d,
+        CX_UNUSED void *d,
         void *mem,
         size_t n
 ) {
@@ -76,7 +76,7 @@
 }
 
 static void *cx_calloc_stdlib(
-        cx_attr_unused void *d,
+        CX_UNUSED void *d,
         size_t nmemb,
         size_t size
 ) {
@@ -84,7 +84,7 @@
 }
 
 static void cx_free_stdlib(
-        cx_attr_unused void *d,
+        CX_UNUSED void *d,
         void *mem
 ) {
     free(mem);
--- a/ucx/array_list.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/array_list.c	Wed Dec 31 16:40:12 2025 +0100
@@ -26,6 +26,10 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
+#ifdef WITH_MEMRCHR
+#define _GNU_SOURCE
+#endif
+
 #include "cx/array_list.h"
 #include "cx/compare.h"
 #include <assert.h>
@@ -97,11 +101,18 @@
     if (index > array->size) return -1;
     if (n == 0) return 0;
 
+    // calculate required capacity
+    size_t req_capacity = array->size + n;
+    if (req_capacity <= array->size) {
+        errno = EOVERFLOW;
+        return -1;
+    }
+
     // guarantee enough capacity
-    if (array->capacity < array->size + n) {
-        const size_t new_capacity = cx_array_grow_capacity(array->capacity,array->size + n);
+    if (array->capacity < req_capacity) {
+        const size_t new_capacity = cx_array_grow_capacity(array->capacity,req_capacity);
         if (cxReallocateArray(allocator, &array->data, new_capacity, elem_size)) {
-            return -1; // LCOV_EXCL_LINE
+            return -1;
         }
         array->capacity = new_capacity;
     }
@@ -111,8 +122,8 @@
     dst += index * elem_size;
 
     // do we need to move some elements?
-    if (index < array->size) {
-        size_t elems_to_move = array->size - index;
+    size_t elems_to_move = array->size - index;
+    if (elems_to_move > 0) {
         char *target = dst + n * elem_size;
         memmove(target, dst, elems_to_move * elem_size);
     }
@@ -127,15 +138,15 @@
     return 0;
 }
 
-int cx_array_insert_sorted_s_(
+int cx_array_insert_sorted_c_(
         const CxAllocator *allocator,
         CxArray *array,
         size_t elem_size,
         const void *sorted_data,
         size_t n,
-        bool allow_duplicates,
         cx_compare_func2 cmp_func,
-        void *context
+        void *context,
+        bool allow_duplicates
 ) {
     // assert pointers
     assert(allocator != NULL);
@@ -310,7 +321,9 @@
                             left_src += elem_size;
                             skip_len++;
                         } else {
-                            break;
+                            // should be unreachable because the requirement is
+                            // that the source array is sorted
+                            break; // LCOV_EXCL_LINE
                         }
                     }
                 }
@@ -339,14 +352,63 @@
         const CxAllocator *allocator,
         CxArray *array,
         size_t elem_size,
-        cx_compare_func cmp_func,
         const void *sorted_data,
         size_t n,
+        cx_compare_func cmp_func,
         bool allow_duplicates
 ) {
     cx_compare_func_wrapper wrapper = {cmp_func};
-    return cx_array_insert_sorted_s_(allocator, array, elem_size, sorted_data,
-        n, allow_duplicates, cx_acmp_wrap, &wrapper);
+    return cx_array_insert_sorted_c_(allocator, array, elem_size, sorted_data,
+        n, cx_cmp_wrap, &wrapper, allow_duplicates);
+}
+
+#ifndef WITH_QSORT_R
+static cx_thread_local cx_compare_func2 cx_array_fn_for_qsort;
+static cx_thread_local void *cx_array_context_for_qsort;
+static int cx_array_qsort_wrapper(const void *l, const void *r) {
+    return cx_array_fn_for_qsort(l, r, cx_array_context_for_qsort);
+}
+#endif
+
+#if defined(WITH_QSORT_R) && defined(__APPLE__)
+// macOS uses a different comparefunc signature for qsort_r
+typedef struct QsortCmpFuncWrapper {
+    cx_compare_func2 fn;
+    void *context;
+} QsortCmpFuncWrapper;
+
+static int sort_comparefunc(void *context, const void *left, const void *right){
+    QsortCmpFuncWrapper *w = context;
+    return w->fn(left, right, w->context);
+}
+#endif
+
+void cx_array_qsort_c(void *array, size_t nmemb, size_t size,
+        cx_compare_func2 fn, void *context) {
+#ifdef WITH_QSORT_R
+#ifndef __APPLE__
+    qsort_r(array, nmemb, size, fn, context);
+#else
+    QsortCmpFuncWrapper wrapper;
+    wrapper.fn = fn;
+    wrapper.context = context;
+    qsort_r(array, nmemb, size, &wrapper, sort_comparefunc);
+#endif
+#else
+    cx_array_fn_for_qsort = fn;
+    cx_array_context_for_qsort = context;
+    qsort(array, nmemb, size, cx_array_qsort_wrapper);
+#endif
+}
+
+void cx_array_sort_(CxArray *array, size_t elem_size,
+        cx_compare_func fn) {
+    qsort(array->data, array->size, elem_size, fn);
+}
+
+void cx_array_sort_c_(CxArray *array, size_t elem_size,
+        cx_compare_func2 fn, void *context) {
+    cx_array_qsort_c(array->data, array->size, elem_size, fn, context);
 }
 
 CxIterator cx_array_iterator_(CxArray *array, size_t elem_size) {
@@ -357,6 +419,50 @@
     return cxIteratorPtr(array->data, array->size);
 }
 
+void cx_array_remove_(CxArray *array, size_t elem_size, size_t index, size_t n, bool fast) {
+    if (n == 0) return;
+    if (index >= array->size) return;
+    if (index + n >= array->size) {
+        // only tail elements are removed
+        array->size = index;
+        return;
+    }
+    array->size -= n;
+    size_t remaining = array->size - index;
+    char *dest = ((char*)array->data) + index * elem_size;
+    if (fast) {
+        char *src = dest + remaining * elem_size;
+        if (n == 1 && elem_size <= CX_WORDSIZE/8) {
+            // try to optimize int-sized values
+            // (from likely to unlikely)
+            if (elem_size == sizeof(int32_t)) {
+                *(int32_t*)dest = *(int32_t*)src;
+                return;
+            }
+#if CX_WORDSIZE == 64
+            if (elem_size == sizeof(int64_t)) {
+                *(int64_t*)dest = *(int64_t*)src;
+                return;
+            }
+#endif
+            if (elem_size == sizeof(int8_t)) {
+                *(int8_t*)dest = *(int8_t*)src;
+                return;
+            }
+            if (elem_size == sizeof(int16_t)) {
+                *(int16_t*)dest = *(int16_t*)src;
+                return;
+            }
+            // note we cannot optimize the last branch, because
+            // the elem_size could be crazily misaligned
+        }
+        memcpy(dest, src, n * elem_size);
+    } else {
+        char *src = dest + n * elem_size;
+        memmove(dest, src, remaining * elem_size);
+    }
+}
+
 void cx_array_free_(const CxAllocator *allocator, CxArray *array) {
     cxFree(allocator, array->data);
     array->data = NULL;
@@ -501,7 +607,7 @@
         cx_compare_func cmp_func
 ) {
     cx_compare_func_wrapper wrapper = {cmp_func};
-    return cx_array_binary_search_inf_c(arr, size, elem_size, elem, cx_acmp_wrap, &wrapper);
+    return cx_array_binary_search_inf_c(arr, size, elem_size, elem, cx_cmp_wrap, &wrapper);
 }
 
 size_t cx_array_binary_search(
@@ -512,7 +618,7 @@
         cx_compare_func cmp_func
 ) {
     cx_compare_func_wrapper wrapper = {cmp_func};
-    return cx_array_binary_search_c(arr, size, elem_size, elem, cx_acmp_wrap, &wrapper);
+    return cx_array_binary_search_c(arr, size, elem_size, elem, cx_cmp_wrap, &wrapper);
 }
 
 size_t cx_array_binary_search_sup(
@@ -523,7 +629,7 @@
         cx_compare_func cmp_func
 ) {
     cx_compare_func_wrapper wrapper = {cmp_func};
-    return cx_array_binary_search_sup_c(arr, size, elem_size, elem, cx_acmp_wrap, &wrapper);
+    return cx_array_binary_search_sup_c(arr, size, elem_size, elem, cx_cmp_wrap, &wrapper);
 }
 
 #ifndef CX_ARRAY_SWAP_SBO_SIZE
@@ -631,15 +737,15 @@
         arl->data, list->collection.size, arl->capacity
     };
 
-    if (cx_array_insert_sorted_s_(
+    if (cx_array_insert_sorted_c_(
             list->collection.allocator,
             &wrap,
             list->collection.elem_size,
             sorted_data,
             n,
-            allow_duplicates,
             cx_list_compare_wrapper,
-            list
+            list,
+            allow_duplicates
     )) {
         // array list implementation is "all or nothing"
         return 0;  // LCOV_EXCL_LINE
@@ -752,10 +858,9 @@
     }
 
     // just move the elements to the left
-    char *first_remaining = arl->data;
-    first_remaining += (index + remove) * list->collection.elem_size;
     char *dst_move = arl->data;
     dst_move += index * list->collection.elem_size;
+    char *first_remaining = dst_move + remove * list->collection.elem_size;
     memmove(dst_move, first_remaining, remaining * list->collection.elem_size);
 
     // decrease the size
@@ -849,19 +954,12 @@
     return list->collection.size;
 }
 
-// TODO: remove this hack once we have a solution for qsort() / qsort_s()
-static _Thread_local CxList *cx_hack_for_qsort_list;
-static int cx_hack_cmp_for_qsort(const void *l, const void *r) {
-    return cx_list_compare_wrapper(l, r, cx_hack_for_qsort_list);
-}
-
 static void cx_arl_sort(struct cx_list_s *list) {
-    // TODO: think about if we can somehow use qsort()_s
-    cx_hack_for_qsort_list = list;
-    qsort(((cx_array_list *) list)->data,
+    cx_array_qsort_c(((cx_array_list *) list)->data,
           list->collection.size,
           list->collection.elem_size,
-          cx_hack_cmp_for_qsort
+          cx_list_compare_wrapper,
+          list
     );
 }
 
@@ -995,8 +1093,7 @@
 
     cx_array_list *list = cxCalloc(allocator, 1, sizeof(cx_array_list));
     if (list == NULL) return NULL;
-    cx_list_init((CxList*)list, &cx_array_list_class,
-        allocator, elem_size);
+    cx_list_init((CxList*)list, &cx_array_list_class, allocator, elem_size);
     list->capacity = initial_capacity;
 
     // allocate the array after the real elem_size is known
--- a/ucx/compare.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/compare.c	Wed Dec 31 16:40:12 2025 +0100
@@ -291,16 +291,7 @@
     }
 }
 
-int cx_acmp_memcmp(
-        const void *ptr1,
-        const void *ptr2,
-        void *size
-) {
-    size_t n = *(size_t*)size;
-    return memcmp(ptr1, ptr2, n);
-}
-
-int cx_acmp_wrap(
+int cx_cmp_wrap(
         const void *ptr1,
         const void *ptr2,
         void *w
--- a/ucx/cx/allocator.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/allocator.h	Wed Dec 31 16:40:12 2025 +0100
@@ -35,10 +35,6 @@
 
 #include "common.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * The class definition for an allocator.
  */
@@ -153,8 +149,8 @@
  *
  * @return the system's memory page size in bytes
  */
-cx_attr_nodiscard
-CX_EXPORT unsigned long cx_system_page_size(void);
+CX_EXTERN CX_NODISCARD
+unsigned long cx_system_page_size(void);
 
 /**
  * Reallocate a previously allocated block.
@@ -167,8 +163,8 @@
  * @retval non-zero failure
  * @see cx_reallocatearray()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT int cx_reallocate_(void **mem, size_t n);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_reallocate_(void **mem, size_t n);
 
 /**
  * Reallocate a previously allocated block.
@@ -182,8 +178,8 @@
  * @retval non-zero failure
  * @see cx_reallocate()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT int cx_reallocatearray_(void **mem, size_t nmemb, size_t size);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_reallocatearray_(void **mem, size_t nmemb, size_t size);
 
 /**
  * Reallocate a previously allocated block and changes the pointer in-place,
@@ -239,8 +235,8 @@
  * @param allocator the allocator
  * @param mem a pointer to the block to free
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT void cxFree(const CxAllocator *allocator, void *mem);
+CX_EXTERN CX_NONNULL_ARG(1)
+void cxFree(const CxAllocator *allocator, void *mem);
 
 /**
  * Allocate @p n bytes of memory.
@@ -249,9 +245,9 @@
  * @param n the number of bytes
  * @return a pointer to the allocated memory
  */
-cx_attr_nodiscard cx_attr_nonnull
-cx_attr_malloc cx_attr_dealloc_ucx cx_attr_allocsize(2)
-CX_EXPORT void *cxMalloc(const CxAllocator *allocator, size_t n);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+CX_MALLOC CX_DEALLOC_UCX CX_ALLOCSIZE(2)
+void *cxMalloc(const CxAllocator *allocator, size_t n);
 
 /**
  * Reallocate the previously allocated block in @p mem, making the new block
@@ -268,9 +264,9 @@
  * @param n the new size in bytes
  * @return a pointer to the reallocated memory
  */
-cx_attr_nodiscard cx_attr_nonnull_arg(1)
-cx_attr_dealloc_ucx cx_attr_allocsize(3)
-CX_EXPORT void *cxRealloc(const CxAllocator *allocator, void *mem, size_t n);
+CX_EXTERN CX_NODISCARD CX_NONNULL_ARG(1)
+CX_DEALLOC_UCX CX_ALLOCSIZE(3)
+void *cxRealloc(const CxAllocator *allocator, void *mem, size_t n);
 
 /**
  * Reallocate the previously allocated block in @p mem.
@@ -292,9 +288,9 @@
  * @param size the size of each element
  * @return a pointer to the reallocated memory
  */
-cx_attr_nodiscard cx_attr_nonnull_arg(1)
-cx_attr_dealloc_ucx cx_attr_allocsize(3, 4)
-CX_EXPORT void *cxReallocArray(const CxAllocator *allocator,
+CX_EXTERN CX_NODISCARD CX_NONNULL_ARG(1)
+CX_DEALLOC_UCX CX_ALLOCSIZE(3, 4)
+void *cxReallocArray(const CxAllocator *allocator,
         void *mem, size_t nmemb, size_t size);
 
 /**
@@ -308,8 +304,8 @@
  * @retval zero success
  * @retval non-zero failure
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT int cxReallocate_(const CxAllocator *allocator, void **mem, size_t n);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+int cxReallocate_(const CxAllocator *allocator, void **mem, size_t n);
 
 /**
  * Reallocate a previously allocated block and changes the pointer in-place,
@@ -342,8 +338,8 @@
  * @retval zero success
  * @retval non-zero on failure
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT int cxReallocateArray_(const CxAllocator *allocator,
+CX_EXTERN CX_NODISCARD CX_NONNULL
+int cxReallocateArray_(const CxAllocator *allocator,
         void **mem, size_t nmemb, size_t size);
 
 /**
@@ -376,9 +372,9 @@
  * @param size the size of each element in bytes
  * @return a pointer to the allocated memory
  */
-cx_attr_nonnull_arg(1) cx_attr_nodiscard
-cx_attr_malloc cx_attr_dealloc_ucx cx_attr_allocsize(2, 3)
-CX_EXPORT void *cxCalloc(const CxAllocator *allocator, size_t nmemb, size_t size);
+CX_EXTERN CX_NONNULL_ARG(1) CX_NODISCARD
+CX_MALLOC CX_DEALLOC_UCX CX_ALLOCSIZE(2, 3)
+void *cxCalloc(const CxAllocator *allocator, size_t nmemb, size_t size);
 
 /**
  * Allocate @p n bytes of memory and sets every byte to zero.
@@ -387,9 +383,9 @@
  * @param n the number of bytes
  * @return a pointer to the allocated memory
  */
-cx_attr_nodiscard cx_attr_nonnull
-cx_attr_malloc cx_attr_dealloc_ucx cx_attr_allocsize(2)
-CX_EXPORT void *cxZalloc(const CxAllocator *allocator, size_t n);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+CX_MALLOC CX_DEALLOC_UCX CX_ALLOCSIZE(2)
+void *cxZalloc(const CxAllocator *allocator, size_t n);
 
 /**
  * Allocate @p n bytes of memory.
@@ -510,10 +506,7 @@
  *
  * @param mem the memory to deallocate
  */
-CX_EXPORT void cxFreeDefault(void *mem);
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN
+void cxFreeDefault(void *mem);
 
 #endif // UCX_ALLOCATOR_H
--- a/ucx/cx/array_list.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/array_list.h	Wed Dec 31 16:40:12 2025 +0100
@@ -39,10 +39,6 @@
 
 #include "list.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * The maximum item size in an array list that fits into
  * a stack buffer when swapped.
@@ -88,8 +84,8 @@
  * @retval zero allocation was successful
  * @retval non-zero allocation failed
  */
-cx_attr_nonnull
-CX_EXPORT int cx_array_init_(const CxAllocator *allocator, CxArray *array, size_t elem_size, size_t capacity);
+CX_EXTERN CX_NONNULL
+int cx_array_init_(const CxAllocator *allocator, CxArray *array, size_t elem_size, size_t capacity);
 
 /**
  * Initializes an array by allocating memory.
@@ -131,8 +127,8 @@
  * @param capacity the capacity of the fixed size array
  * @param size the number of initialized elements in the fixed size array
  */
-cx_attr_nonnull
-CX_EXPORT void cx_array_init_fixed_(CxArray *array, const void *data, size_t capacity, size_t size);
+CX_EXTERN CX_NONNULL
+void cx_array_init_fixed_(CxArray *array, const void *data, size_t capacity, size_t size);
 
 /**
  * Initializes an array with fixed size memory.
@@ -173,8 +169,8 @@
  * @retval zero allocation was successful
  * @retval non-zero allocation failed
  */
-cx_attr_nonnull
-CX_EXPORT int cx_array_reserve_(const CxAllocator *allocator, CxArray *array, size_t elem_size, size_t capacity);
+CX_EXTERN CX_NONNULL
+int cx_array_reserve_(const CxAllocator *allocator, CxArray *array, size_t elem_size, size_t capacity);
 
 /**
  * Changes the capacity of an array.
@@ -215,8 +211,8 @@
  * @retval zero allocation was successful
  * @retval non-zero allocation failed
  */
-cx_attr_nonnull
-CX_EXPORT int cx_array_copy_to_new_(const CxAllocator *allocator, CxArray *array, size_t elem_size, size_t capacity);
+CX_EXTERN CX_NONNULL
+int cx_array_copy_to_new_(const CxAllocator *allocator, CxArray *array, size_t elem_size, size_t capacity);
 
 /**
  * Copies the array to a new memory region.
@@ -269,8 +265,8 @@
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-cx_attr_nonnull_arg(1, 2)
-CX_EXPORT int cx_array_insert_(const CxAllocator *allocator, CxArray *array,
+CX_EXTERN CX_NONNULL_ARG(1, 2)
+int cx_array_insert_(const CxAllocator *allocator, CxArray *array,
         size_t elem_size, size_t index, const void *other, size_t n);
 
 /**
@@ -397,17 +393,17 @@
  * @param allocator the allocator to use for a possible reallocation
  * @param array a pointer to the array structure
  * @param elem_size the size of one element
- * @param cmp_func
  * @param sorted_data a pointer to an array of data that shall be inserted
  * @param n the number of elements that shall be inserted
+ * @param cmp_func the compare function
  * @param allow_duplicates @c false if duplicates shall be skipped during insertion
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-cx_attr_nonnull
-CX_EXPORT int cx_array_insert_sorted_(const CxAllocator *allocator, CxArray *array,
-        size_t elem_size, cx_compare_func cmp_func, const void *sorted_data, size_t n,
-        bool allow_duplicates);
+CX_EXTERN CX_NONNULL
+int cx_array_insert_sorted_(const CxAllocator *allocator, CxArray *array,
+        size_t elem_size, const void *sorted_data, size_t n,
+        cx_compare_func cmp_func, bool allow_duplicates);
 
 /**
  * Inserts an element into a sorted array.
@@ -418,13 +414,13 @@
  *
  * @param allocator (@c CxAllocator*) the allocator to use for a possible reallocation
  * @param array the name of the array where the elements shall be inserted
+ * @param element the element that shall be inserted
  * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
- * @param element (@c void*) a pointer to element that shall be inserted
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-#define cx_array_insert_sorted_a(allocator, array, cmp_func, element) \
-        cx_array_insert_sorted_(allocator, (CxArray*)&(array), sizeof((array).data[0]), cmp_func, element, 1, true)
+#define cx_array_insert_sorted_a(allocator, array, element, cmp_func) \
+        cx_array_insert_sorted_(allocator, (CxArray*)&(array), sizeof((array).data[0]), (void*)&(element), 1, cmp_func, true)
 
 /**
  * Inserts an element into a sorted array.
@@ -434,13 +430,13 @@
  * @attention if the array is not sorted according to the specified @p cmp_func, the behavior is undefined.
  *
  * @param array the name of the array where the elements shall be inserted
+ * @param element the element that shall be inserted
  * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
- * @param element (@c void*) a pointer to element that shall be inserted
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-#define cx_array_insert_sorted(array, cmp_func, element) \
-        cx_array_insert_sorted_a(cxDefaultAllocator, array, cmp_func, element)
+#define cx_array_insert_sorted(array, element, cmp_func) \
+        cx_array_insert_sorted_a(cxDefaultAllocator, array, element, cmp_func)
 
 /**
  * Inserts sorted data into a sorted array.
@@ -451,14 +447,14 @@
  *
  * @param allocator (@c CxAllocator*) the allocator to use for a possible reallocation
  * @param array the name of the array where the elements shall be inserted
- * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
  * @param sorted_data (@c void*) a pointer to an array of sorted data that shall be inserted
  * @param n (@c size_t) the number of elements that shall be inserted
+ * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-#define cx_array_insert_sorted_array_a(allocator, array, cmp_func, sorted_data, n) \
-        cx_array_insert_sorted_(allocator, (CxArray*)&(array), sizeof((array).data[0]), cmp_func, sorted_data, n, true)
+#define cx_array_insert_sorted_array_a(allocator, array, sorted_data, n, cmp_func) \
+        cx_array_insert_sorted_(allocator, (CxArray*)&(array), sizeof((array).data[0]), sorted_data, n, cmp_func, true)
 
 /**
  * Inserts sorted data into a sorted array.
@@ -468,14 +464,14 @@
  * @attention if either array is not sorted according to the specified @p cmp_func, the behavior is undefined.
  *
  * @param array the name of the array where the elements shall be inserted
- * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
  * @param sorted_data (@c void*) a pointer to an array of sorted data that shall be inserted
  * @param n (@c size_t) the number of elements that shall be inserted
+ * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-#define cx_array_insert_sorted_array(array, cmp_func, sorted_data, n) \
-        cx_array_insert_sorted_array_a(cxDefaultAllocator, array, cmp_func, sorted_data, n)
+#define cx_array_insert_sorted_array(array, sorted_data, n, cmp_func) \
+        cx_array_insert_sorted_array_a(cxDefaultAllocator, array, sorted_data, n, cmp_func)
 
 /**
  * Inserts an element into a sorted array if it is not already contained.
@@ -486,13 +482,13 @@
  *
  * @param allocator (@c CxAllocator*) the allocator to use for a possible reallocation
  * @param array the name of the array where the elements shall be inserted
+ * @param element the element that shall be inserted
  * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
- * @param element (@c void*) a pointer to element that shall be inserted
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-#define cx_array_insert_unique_a(allocator, array, cmp_func, element) \
-        cx_array_insert_sorted_(allocator, (CxArray*)&(array), sizeof((array).data[0]), cmp_func, element, 1, false)
+#define cx_array_insert_unique_a(allocator, array, element, cmp_func) \
+        cx_array_insert_sorted_(allocator, (CxArray*)&(array), sizeof((array).data[0]), (void*)&(element), 1, cmp_func, false)
 
 /**
  * Inserts an element into a sorted array if it is not already contained.
@@ -502,13 +498,13 @@
  * @attention if the array is not sorted according to the specified @p cmp_func, the behavior is undefined.
  *
  * @param array the name of the array where the elements shall be inserted
+ * @param element the element that shall be inserted
  * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
- * @param element (@c void*) a pointer to element that shall be inserted
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-#define cx_array_insert_unique(array, cmp_func, element) \
-        cx_array_insert_unique_a(cxDefaultAllocator, array, cmp_func, element)
+#define cx_array_insert_unique(array, element, cmp_func) \
+        cx_array_insert_unique_a(cxDefaultAllocator, array, element, cmp_func)
 
 /**
  * Inserts sorted data into a sorted array, skipping duplicates.
@@ -519,14 +515,14 @@
  *
  * @param allocator (@c CxAllocator*) the allocator to use for a possible reallocation
  * @param array the name of the array where the elements shall be inserted
- * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
  * @param sorted_data (@c void*) a pointer to an array of sorted data that shall be inserted
  * @param n (@c size_t) the number of elements that shall be inserted
+ * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-#define cx_array_insert_unique_array_a(allocator, array, cmp_func, sorted_data, n) \
-        cx_array_insert_sorted_(allocator, (CxArray*)&(array), sizeof((array).data[0]), cmp_func, sorted_data, n, false)
+#define cx_array_insert_unique_array_a(allocator, array, sorted_data, n, cmp_func) \
+        cx_array_insert_sorted_(allocator, (CxArray*)&(array), sizeof((array).data[0]), sorted_data, n, cmp_func, false)
 
 /**
  * Inserts sorted data into a sorted array, skipping duplicates.
@@ -536,14 +532,240 @@
  * @attention if either array is not sorted according to the specified @p cmp_func, the behavior is undefined.
  *
  * @param array the name of the array where the elements shall be inserted
- * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
  * @param sorted_data (@c void*) a pointer to an array of sorted data that shall be inserted
  * @param n (@c size_t) the number of elements that shall be inserted
+ * @param cmp_func (@c cx_compare_func) the compare function that establishes the order
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+#define cx_array_insert_unique_array(array, sorted_data, n, cmp_func) \
+        cx_array_insert_unique_array_a(cxDefaultAllocator, array, sorted_data, n, cmp_func)
+
+/**
+ * Inserts sorted data into a sorted array.
+ *
+ * Internal function - do not use.
+ *
+ * @param allocator the allocator to use for a possible reallocation
+ * @param array a pointer to the array structure
+ * @param elem_size the size of one element
+ * @param sorted_data a pointer to an array of data that shall be inserted
+ * @param n the number of elements that shall be inserted
+ * @param cmp_func the compare function
+ * @param context additional context for the compare function
+ * @param allow_duplicates @c false if duplicates shall be skipped during insertion
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+CX_EXTERN CX_NONNULL_ARG(1, 2, 4, 6)
+int cx_array_insert_sorted_c_(const CxAllocator *allocator, CxArray *array,
+        size_t elem_size, const void *sorted_data, size_t n,
+        cx_compare_func2 cmp_func, void *context, bool allow_duplicates);
+
+/**
+ * Inserts an element into a sorted array.
+ *
+ * When the capacity is not enough to hold the new element, a re-allocation is attempted.
+ *
+ * @attention if the array is not sorted according to the specified @p cmp_func, the behavior is undefined.
+ *
+ * @param allocator (@c CxAllocator*) the allocator to use for a possible reallocation
+ * @param array the name of the array where the elements shall be inserted
+ * @param element the element that shall be inserted
+ * @param cmp_func (@c cx_compare_func2) the compare function that establishes the order
+ * @param context (@c void*) additional context for the compare function
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+#define cx_array_insert_sorted_ca(allocator, array, element, cmp_func, context) \
+        cx_array_insert_sorted_c_(allocator, (CxArray*)&(array), sizeof((array).data[0]), (void*)&(element), 1, cmp_func, context, true)
+
+/**
+ * Inserts an element into a sorted array.
+ *
+ * When the capacity is not enough to hold the new element, a re-allocation is attempted.
+ *
+ * @attention if the array is not sorted according to the specified @p cmp_func, the behavior is undefined.
+ *
+ * @param array the name of the array where the elements shall be inserted
+ * @param element the element that shall be inserted
+ * @param cmp_func (@c cx_compare_func2) the compare function that establishes the order
+ * @param context (@c void*) additional context for the compare function
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+#define cx_array_insert_sorted_c(array, element, cmp_func, context) \
+        cx_array_insert_sorted_ca(cxDefaultAllocator, array, element, cmp_func, context)
+
+/**
+ * Inserts sorted data into a sorted array.
+ *
+ * When the capacity is not enough to hold the new elements, a re-allocation is attempted.
+ *
+ * @attention if either array is not sorted according to the specified @p cmp_func, the behavior is undefined.
+ *
+ * @param allocator (@c CxAllocator*) the allocator to use for a possible reallocation
+ * @param array the name of the array where the elements shall be inserted
+ * @param sorted_data (@c void*) a pointer to an array of sorted data that shall be inserted
+ * @param n (@c size_t) the number of elements that shall be inserted
+ * @param cmp_func (@c cx_compare_func2) the compare function that establishes the order
+ * @param context (@c void*) additional context for the compare function
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+#define cx_array_insert_sorted_array_ca(allocator, array, sorted_data, n, cmp_func, context) \
+        cx_array_insert_sorted_c_(allocator, (CxArray*)&(array), sizeof((array).data[0]), sorted_data, n, cmp_func, context, true)
+
+/**
+ * Inserts sorted data into a sorted array.
+ *
+ * When the capacity is not enough to hold the new elements, a re-allocation is attempted.
+ *
+ * @attention if either array is not sorted according to the specified @p cmp_func, the behavior is undefined.
+ *
+ * @param array the name of the array where the elements shall be inserted
+ * @param sorted_data (@c void*) a pointer to an array of sorted data that shall be inserted
+ * @param n (@c size_t) the number of elements that shall be inserted
+ * @param cmp_func (@c cx_compare_func2) the compare function that establishes the order
+ * @param context (@c void*) additional context for the compare function
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+#define cx_array_insert_sorted_array_c(array, sorted_data, n, cmp_func, context) \
+        cx_array_insert_sorted_array_ca(cxDefaultAllocator, array, sorted_data, n, cmp_func, context)
+
+/**
+ * Inserts an element into a sorted array if it is not already contained.
+ *
+ * When the capacity is not enough to hold the new element, a re-allocation is attempted.
+ *
+ * @attention if the array is not sorted according to the specified @p cmp_func, the behavior is undefined.
+ *
+ * @param allocator (@c CxAllocator*) the allocator to use for a possible reallocation
+ * @param array the name of the array where the elements shall be inserted
+ * @param element the element that shall be inserted
+ * @param cmp_func (@c cx_compare_func2) the compare function that establishes the order
+ * @param context (@c void*) additional context for the compare function
  * @retval zero success
  * @retval non-zero a re-allocation was necessary but failed
  */
-#define cx_array_insert_unique_array(array, cmp_func, sorted_data, n) \
-        cx_array_insert_unique_array_a(cxDefaultAllocator, array, cmp_func, sorted_data, n)
+#define cx_array_insert_unique_ca(allocator, array, element, cmp_func, context) \
+        cx_array_insert_sorted_c_(allocator, (CxArray*)&(array), sizeof((array).data[0]), (void*)&(element), 1, cmp_func, context, false)
+
+/**
+ * Inserts an element into a sorted array if it is not already contained.
+ *
+ * When the capacity is not enough to hold the new element, a re-allocation is attempted.
+ *
+ * @attention if the array is not sorted according to the specified @p cmp_func, the behavior is undefined.
+ *
+ * @param array the name of the array where the elements shall be inserted
+ * @param element the element that shall be inserted
+ * @param cmp_func (@c cx_compare_func2) the compare function that establishes the order
+ * @param context (@c void*) additional context for the compare function
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+#define cx_array_insert_unique_c(array, element, cmp_func, context) \
+        cx_array_insert_unique_ca(cxDefaultAllocator, array, element, cmp_func, context)
+
+/**
+ * Inserts sorted data into a sorted array, skipping duplicates.
+ *
+ * When the capacity is not enough to hold the new elements, a re-allocation is attempted.
+ *
+ * @attention if either array is not sorted according to the specified @p cmp_func, the behavior is undefined.
+ *
+ * @param allocator (@c CxAllocator*) the allocator to use for a possible reallocation
+ * @param array the name of the array where the elements shall be inserted
+ * @param sorted_data (@c void*) a pointer to an array of sorted data that shall be inserted
+ * @param n (@c size_t) the number of elements that shall be inserted
+ * @param cmp_func (@c cx_compare_func2) the compare function that establishes the order
+ * @param context (@c void*) additional context for the compare function
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+#define cx_array_insert_unique_array_ca(allocator, array, sorted_data, n, cmp_func, context) \
+        cx_array_insert_sorted_c_(allocator, (CxArray*)&(array), sizeof((array).data[0]), sorted_data, n, cmp_func, context, false)
+
+/**
+ * Inserts sorted data into a sorted array, skipping duplicates.
+ *
+ * When the capacity is not enough to hold the new elements, a re-allocation is attempted.
+ *
+ * @attention if either array is not sorted according to the specified @p cmp_func, the behavior is undefined.
+ *
+ * @param array the name of the array where the elements shall be inserted
+ * @param sorted_data (@c void*) a pointer to an array of sorted data that shall be inserted
+ * @param n (@c size_t) the number of elements that shall be inserted
+ * @param cmp_func (@c cx_compare_func2) the compare function that establishes the order
+ * @param context (@c void*) additional context for the compare function
+ * @retval zero success
+ * @retval non-zero a re-allocation was necessary but failed
+ */
+#define cx_array_insert_unique_array_c(array, sorted_data, n, cmp_func, context) \
+        cx_array_insert_unique_array_ca(cxDefaultAllocator, array, sorted_data, n, cmp_func, context)
+
+/**
+ * An alternative to qsort_r() when that is not available on your platform.
+ *
+ * If it is available, qsort_r() is used directly.
+ *
+ * @param array the array that shall be sorted
+ * @param nmemb the number of elements in the array
+ * @param size the size of one element
+ * @param fn the compare function
+ * @param context the context for the compare function
+ */
+CX_EXTERN CX_NONNULL
+void cx_array_qsort_c(void *array, size_t nmemb, size_t size,
+        cx_compare_func2 fn, void *context);
+
+/**
+ * Sorts an array.
+ *
+ * Internal function - do not use.
+ *
+ * @param array a pointer to the array structure
+ * @param elem_size the size of one element
+ * @param fn the compare function
+ */
+CX_EXTERN CX_NONNULL
+void cx_array_sort_(CxArray *array, size_t elem_size,
+        cx_compare_func fn);
+
+/**
+ * Sorts an array.
+ *
+ * Internal function - do not use.
+ *
+ * @param array a pointer to the array structure
+ * @param elem_size the size of one element
+ * @param fn the compare function
+ * @param context the context for the compare function
+ */
+CX_EXTERN CX_NONNULL
+void cx_array_sort_c_(CxArray *array, size_t elem_size,
+        cx_compare_func2 fn, void *context);
+
+/**
+ * Sorts an array.
+ *
+ * @param array the name of the array
+ * @param fn (@c cx_compare_func) the compare function
+ */
+#define cx_array_sort(array, fn) \
+        cx_array_sort_((CxArray*)&(array), sizeof((array).data[0]), fn)
+
+/**
+ * Sorts an array.
+ *
+ * @param array the name of the array
+ * @param fn (@c cx_compare_func2) the compare function
+ * @param context (@c void*) the context for the compare function
+ */
+#define cx_array_sort_c(array, fn, context) \
+        cx_array_sort_c_((CxArray*)&(array), sizeof((array).data[0]), fn, context)
 
 /**
  * Creates an iterator over the elements of an array.
@@ -554,14 +776,17 @@
  * @param elem_size the size of one element
  * @return an iterator over the elements
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT CxIterator cx_array_iterator_(CxArray *array, size_t elem_size);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+CxIterator cx_array_iterator_(CxArray *array, size_t elem_size);
 
 /**
  * Creates an iterator over the elements of an array.
  *
  * The iterator will yield pointers to the elements.
  *
+ * This iterator cannot be used to remove elements
+ * because it does not get a modifiable reference to the array's size.
+ *
  * @param array the name of the array
  * @return an iterator over the elements
  * @see cx_array_iterator_ptr()
@@ -577,8 +802,8 @@
  * @param array the name of the array
  * @return an iterator over the elements
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT CxIterator cx_array_iterator_ptr_(CxArray *array);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+CxIterator cx_array_iterator_ptr_(CxArray *array);
 
 /**
  * Creates an iterator over the elements of an array containing pointers.
@@ -586,6 +811,9 @@
  * The iterator will yield the elements themselves, which are supposed to
  * be pointers.
  *
+ * This iterator cannot be used to remove elements
+ * because it does not get a modifiable reference to the array's size.
+ *
  * @param array the name of the array
  * @return an iterator over the elements
  * @see cx_array_iterator()
@@ -593,6 +821,87 @@
 #define cx_array_iterator_ptr(array) \
         cx_array_iterator_ptr_((CxArray*)&(array))
 
+
+/**
+ * Removes elements from the array.
+ *
+ * Internal function - do not use.
+ *
+ * @param array a pointer to the array structure
+ * @param elem_size the size of one element
+ * @param index the index of the first element to remove
+ * @param n the number of elements to remove
+ * @param fast indicates whether tail elements should be copied into the gap
+ */
+CX_EXTERN CX_NONNULL
+void cx_array_remove_(CxArray *array, size_t elem_size, size_t index, size_t n, bool fast);
+
+/**
+ * Removes one element from the array.
+ *
+ * Tail elements are all moved by one. If you don't need a stable order
+ * in the array, consider using cx_array_remove_fast().
+ *
+ * If the index is out of bounds, this function does nothing.
+ *
+ * @param array the name of the array
+ * @param index (@c size_t) the index of the element to remove
+ * @see cx_array_remove_fast()
+ */
+#define cx_array_remove(array, index) \
+        cx_array_remove_((CxArray*)&(array), sizeof((array).data[0]), index, 1, false)
+
+/**
+ * Removes one element from the array.
+ *
+ * The gap will be filled with a copy of the last element in the array.
+ * This changes the order of elements. If you want a stable order,
+ * use cx_array_remove() instead.
+ *
+ * If the index is out of bounds, this function does nothing.
+ *
+ * @param array the name of the array
+ * @param index (@c size_t) the index of the element to remove
+ * @see cx_array_remove()
+ */
+#define cx_array_remove_fast(array, index) \
+        cx_array_remove_((CxArray*)&(array), sizeof((array).data[0]), index, 1, true)
+
+/**
+ * Removes multiple elements from the array.
+ *
+ * Tail elements are all moved to close the gap. If you don't need a stable
+ * order in the array, consider using cx_array_remove_array_fast().
+ *
+ * If the index is out of bounds, this function does nothing.
+ * If @n overflows the array, this function removes as many elements as it can.
+ *
+ * @param array the name of the array
+ * @param index (@c size_t) the index of the first element to remove
+ * @param n (@c size_t) the number of elements to remove
+ * @see cx_array_remove_array_fast()
+ */
+#define cx_array_remove_array(array, index, n) \
+        cx_array_remove_((CxArray*)&(array), sizeof((array).data[0]), index, n, false)
+
+/**
+ * Removes multiple elements from the array.
+ *
+ * Tail elements are copied into the gap. If you have more tail elements
+ * than the number of elements that are removed, this will change the order
+ * of elements. If you want a stable order, use cx_array_remove_array() instead.
+ *
+ * If the index is out of bounds, this function does nothing.
+ * If @n overflows the array, this function removes as many elements as it can.
+ *
+ * @param array the name of the array
+ * @param index (@c size_t) the index of the first element to remove
+ * @param n (@c size_t) the number of elements to remove
+ * @see cx_array_remove_array()
+ */
+#define cx_array_remove_array_fast(array, index, n) \
+        cx_array_remove_((CxArray*)&(array), sizeof((array).data[0]), index, n, true)
+
 /**
  * Deallocates an array.
  *
@@ -601,8 +910,8 @@
  * @param allocator (@c CxAllocator*) the allocator which was used to allocate the array
  * @param array a pointer to the array structure
  */
-cx_attr_nonnull
-CX_EXPORT void cx_array_free_(const CxAllocator *allocator, CxArray *array);
+CX_EXTERN CX_NONNULL
+void cx_array_free_(const CxAllocator *allocator, CxArray *array);
 
 /**
  * Deallocates an array.
@@ -651,8 +960,8 @@
  * @see cx_array_binary_search_sup()
  * @see cx_array_binary_search()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_array_binary_search_inf(const void *arr, size_t size,
+CX_EXTERN CX_NONNULL
+size_t cx_array_binary_search_inf(const void *arr, size_t size,
         size_t elem_size, const void *elem, cx_compare_func cmp_func);
 
 /**
@@ -674,8 +983,8 @@
  * @see cx_array_binary_search_inf()
  * @see cx_array_binary_search_sup()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_array_binary_search(const void *arr, size_t size,
+CX_EXTERN CX_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);
 
 /**
@@ -703,8 +1012,8 @@
  * @see cx_array_binary_search_inf()
  * @see cx_array_binary_search()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_array_binary_search_sup(const void *arr, size_t size,
+CX_EXTERN CX_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);
 
 
@@ -734,8 +1043,8 @@
  * @see cx_array_binary_search_sup()
  * @see cx_array_binary_search()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_array_binary_search_inf_c(const void *arr, size_t size,
+CX_EXTERN CX_NONNULL
+size_t cx_array_binary_search_inf_c(const void *arr, size_t size,
         size_t elem_size, const void *elem, cx_compare_func2 cmp_func, void *context);
 
 /**
@@ -758,8 +1067,8 @@
  * @see cx_array_binary_search_inf()
  * @see cx_array_binary_search_sup()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_array_binary_search_c(const void *arr, size_t size,
+CX_EXTERN CX_NONNULL
+size_t cx_array_binary_search_c(const void *arr, size_t size,
         size_t elem_size, const void *elem, cx_compare_func2 cmp_func, void *context);
 
 /**
@@ -788,8 +1097,8 @@
  * @see cx_array_binary_search_inf()
  * @see cx_array_binary_search()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_array_binary_search_sup_c(const void *arr, size_t size,
+CX_EXTERN CX_NONNULL
+size_t cx_array_binary_search_sup_c(const void *arr, size_t size,
         size_t elem_size, const void *elem, cx_compare_func2 cmp_func, void *context);
 
 /**
@@ -800,8 +1109,8 @@
  * @param idx1 index of the first element
  * @param idx2 index of the second element
  */
-cx_attr_nonnull
-CX_EXPORT void cx_array_swap(void *arr, size_t elem_size, size_t idx1, size_t idx2);
+CX_EXTERN CX_NONNULL
+void cx_array_swap(void *arr, size_t elem_size, size_t idx1, size_t idx2);
 
 /**
  * Allocates an array list for storing elements with @p elem_size bytes each.
@@ -816,14 +1125,8 @@
  * @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)
-CX_EXPORT CxList *cxArrayListCreate(const CxAllocator *allocator,
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxListFree, 1)
+CxList *cxArrayListCreate(const CxAllocator *allocator,
         size_t elem_size, size_t initial_capacity);
 
-#ifdef __cplusplus
-} // extern "C"
-#endif
-
 #endif // UCX_ARRAY_LIST_H
--- a/ucx/cx/buffer.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/buffer.h	Wed Dec 31 16:40:12 2025 +0100
@@ -50,10 +50,6 @@
 #include "allocator.h"
 #include "string.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * No buffer features enabled (all flags cleared).
  */
@@ -177,8 +173,8 @@
  * @param flags buffer features (see cx_buffer_s.flags)
  * @return zero on success, non-zero if a required allocation failed
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT int cxBufferInit(CxBuffer *buffer, const CxAllocator *allocator,
+CX_EXTERN CX_NONNULL_ARG(1)
+int cxBufferInit(CxBuffer *buffer, const CxAllocator *allocator,
         void *space, size_t capacity, int flags);
 
 /**
@@ -190,8 +186,8 @@
  * @param buffer the buffer which contents shall be destroyed
  * @see cxBufferInit()
  */
-cx_attr_nonnull
-CX_EXPORT void cxBufferDestroy(CxBuffer *buffer);
+CX_EXTERN CX_NONNULL
+void cxBufferDestroy(CxBuffer *buffer);
 
 /**
  * Deallocates the buffer.
@@ -202,7 +198,8 @@
  * @param buffer the buffer to deallocate
  * @see cxBufferCreate()
  */
-CX_EXPORT void cxBufferFree(CxBuffer *buffer);
+CX_EXTERN
+void cxBufferFree(CxBuffer *buffer);
 
 /**
  * Allocates and initializes a fresh buffer.
@@ -228,8 +225,8 @@
  * @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
-CX_EXPORT CxBuffer *cxBufferCreate(const CxAllocator *allocator, void *space,
+CX_EXTERN CX_MALLOC CX_DEALLOC(cxBufferFree, 1) CX_NODISCARD
+CxBuffer *cxBufferCreate(const CxAllocator *allocator, void *space,
                                    size_t capacity, int flags);
 
 /**
@@ -268,8 +265,8 @@
  * @see cxBufferShiftLeft()
  * @see cxBufferShiftRight()
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferShift(CxBuffer *buffer, off_t shift);
+CX_EXTERN CX_NONNULL
+int cxBufferShift(CxBuffer *buffer, off_t shift);
 
 /**
  * Shifts the buffer to the right.
@@ -281,8 +278,8 @@
  * @retval non-zero if a required auto-extension or copy-on-write fails
  * @see cxBufferShift()
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferShiftRight(CxBuffer *buffer, size_t shift);
+CX_EXTERN CX_NONNULL
+int cxBufferShiftRight(CxBuffer *buffer, size_t shift);
 
 /**
  * Shifts the buffer to the left.
@@ -294,8 +291,8 @@
  * @retval non-zero if the buffer uses copy-on-write and the allocation fails
  * @see cxBufferShift()
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferShiftLeft(CxBuffer *buffer, size_t shift);
+CX_EXTERN CX_NONNULL
+int cxBufferShiftLeft(CxBuffer *buffer, size_t shift);
 
 
 /**
@@ -318,8 +315,8 @@
  * @retval non-zero if the position is invalid
  *
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferSeek(CxBuffer *buffer, off_t offset, int whence);
+CX_EXTERN CX_NONNULL
+int cxBufferSeek(CxBuffer *buffer, off_t offset, int whence);
 
 /**
  * Discards items from the end of the buffer.
@@ -332,8 +329,8 @@
  * @param nitems the number of items to discard
  * @return the actual number of discarded items
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxBufferPop(CxBuffer *buffer, size_t size, size_t nitems);
+CX_EXTERN CX_NONNULL
+size_t cxBufferPop(CxBuffer *buffer, size_t size, size_t nitems);
 
 /**
  * Clears the buffer by resetting the position and deleting the data.
@@ -347,8 +344,8 @@
  * @param buffer the buffer to be cleared
  * @see cxBufferReset()
  */
-cx_attr_nonnull
-CX_EXPORT void cxBufferClear(CxBuffer *buffer);
+CX_EXTERN CX_NONNULL
+void cxBufferClear(CxBuffer *buffer);
 
 /**
  * Resets the buffer by resetting the position and size to zero.
@@ -359,8 +356,8 @@
  * @param buffer the buffer to be cleared
  * @see cxBufferClear()
  */
-cx_attr_nonnull
-CX_EXPORT void cxBufferReset(CxBuffer *buffer);
+CX_EXTERN CX_NONNULL
+void cxBufferReset(CxBuffer *buffer);
 
 /**
  * Tests, if the buffer position has exceeded the buffer size.
@@ -370,8 +367,8 @@
  * byte of the buffer's contents
  * @retval false otherwise
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT bool cxBufferEof(const CxBuffer *buffer);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+bool cxBufferEof(const CxBuffer *buffer);
 
 /**
  * Ensures that the buffer has the required capacity.
@@ -391,8 +388,8 @@
  * @see cxBufferShrink()
  * @see cxBufferMinimumCapacity()
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferReserve(CxBuffer *buffer, size_t capacity);
+CX_EXTERN CX_NONNULL
+int cxBufferReserve(CxBuffer *buffer, size_t capacity);
 
 /**
  * Limits the buffer's capacity.
@@ -410,8 +407,8 @@
  * @see cxBufferReserve()
  * @see cxBufferMinimumCapacity()
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity);
+CX_EXTERN CX_NONNULL
+int cxBufferMaximumCapacity(CxBuffer *buffer, size_t capacity);
 
 /**
  * Ensures that the buffer has a minimum capacity.
@@ -430,8 +427,8 @@
  * @see cxBufferReserve()
  * @see cxBufferShrink()
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferMinimumCapacity(CxBuffer *buffer, size_t capacity);
+CX_EXTERN CX_NONNULL
+int cxBufferMinimumCapacity(CxBuffer *buffer, size_t capacity);
 
 /**
  * Shrinks the capacity of the buffer to fit its current size.
@@ -450,8 +447,8 @@
  * @see cxBufferReserve()
  * @see cxBufferMinimumCapacity()
  */
-cx_attr_nonnull
-CX_EXPORT void cxBufferShrink(CxBuffer *buffer, size_t reserve);
+CX_EXTERN CX_NONNULL
+void cxBufferShrink(CxBuffer *buffer, size_t reserve);
 
 /**
  * Writes data to a CxBuffer.
@@ -474,8 +471,8 @@
  * @see cxBufferAppend()
  * @see cxBufferRead()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxBufferWrite(const void *ptr, size_t size,
+CX_EXTERN CX_NONNULL
+size_t cxBufferWrite(const void *ptr, size_t size,
         size_t nitems, CxBuffer *buffer);
 
 /**
@@ -497,8 +494,8 @@
  * @see cxBufferWrite()
  * @see cxBufferRead()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxBufferAppend(const void *ptr, size_t size,
+CX_EXTERN CX_NONNULL
+size_t cxBufferAppend(const void *ptr, size_t size,
         size_t nitems, CxBuffer *buffer);
 
 /**
@@ -516,8 +513,8 @@
  * @see cxBufferWrite()
  * @see cxBufferAppend()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxBufferRead(void *ptr, size_t size,
+CX_EXTERN CX_NONNULL
+size_t cxBufferRead(void *ptr, size_t size,
         size_t nitems, CxBuffer *buffer);
 
 /**
@@ -540,8 +537,8 @@
  * stream is reached, and automatic extension is not enabled or not possible
  * @see cxBufferTerminate()
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferPut(CxBuffer *buffer, int c);
+CX_EXTERN CX_NONNULL
+int cxBufferPut(CxBuffer *buffer, int c);
 
 /**
  * Writes a terminating zero to a buffer at the current position.
@@ -555,8 +552,8 @@
  * @return zero, if the terminator could be written, non-zero otherwise
  * @see cxBufferShrink()
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferTerminate(CxBuffer *buffer);
+CX_EXTERN CX_NONNULL
+int cxBufferTerminate(CxBuffer *buffer);
 
 /**
  * Internal function - do not use.
@@ -566,8 +563,8 @@
  * @return the number of bytes written
  * @see cxBufferPutString()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_buffer_put_string(CxBuffer *buffer, cxstring str);
+CX_EXTERN CX_NONNULL
+size_t cx_buffer_put_string(CxBuffer *buffer, cxstring str);
 
 /**
  * Writes a string to a buffer with cxBufferWrite().
@@ -588,8 +585,8 @@
  * @return the number of bytes written
  * @see cxBufferPutString()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_buffer_append_string(CxBuffer *buffer, cxstring str);
+CX_EXTERN CX_NONNULL
+size_t cx_buffer_append_string(CxBuffer *buffer, cxstring str);
 
 /**
  * Appends a string to a buffer with cxBufferAppend().
@@ -610,11 +607,29 @@
  * @param buffer the buffer to read from
  * @return the character or @c EOF, if the end of the buffer is reached
  */
-cx_attr_nonnull
-CX_EXPORT int cxBufferGet(CxBuffer *buffer);
+CX_EXTERN CX_NONNULL
+int cxBufferGet(CxBuffer *buffer);
 
-#ifdef __cplusplus
+/**
+ * Gets the data in a buffer as a @c cxstring.
+ *
+ * @param buffer the buffer
+ * @return the data in the buffer interpreted as a @c cxstring
+ */
+CX_NONNULL CX_INLINE
+cxstring cx_bstr(CxBuffer *buffer) {
+    return cx_strn(buffer->space, buffer->size);
 }
-#endif
+
+/**
+ * Gets the data in a buffer as a @c cxmutstr.
+ *
+ * @param buffer the buffer
+ * @return the data in the buffer interpreted as a @c cxmutstr
+ */
+CX_NONNULL CX_INLINE
+cxmutstr cx_bstr_m(CxBuffer *buffer) {
+    return cx_mutstrn(buffer->space, buffer->size);
+}
 
 #endif // UCX_BUFFER_H
--- a/ucx/cx/collection.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/collection.h	Wed Dec 31 16:40:12 2025 +0100
@@ -40,10 +40,6 @@
 #include "iterator.h"
 #include "compare.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * Special constant used for creating collections that are storing pointers.
  */
@@ -204,7 +200,7 @@
  */
 #define cxSetAdvancedCompareFunc(c, func, data) \
     (c)->collection.advanced_cmp = (cx_compare_func2) func; \
-    (c)->collection.destructor_data = data
+    (c)->collection.cmp_data = data
 
 /**
  * Invokes the simple comparator function for two elements.
@@ -316,8 +312,22 @@
     if ((c)->collection.simple_destructor) cx_invoke_simple_destructor(c,e); \
     if ((c)->collection.advanced_destructor) cx_invoke_advanced_destructor(c,e)
 
-#ifdef __cplusplus
-} // extern "C"
-#endif
+/**
+ * Invokes all available destructor functions for a specific element.
+ *
+ * Usually only used by collection implementations. There should be no need
+ * to invoke this macro manually.
+ *
+ * In contrast to cx_invoke_destructor(), this macro does not automatically
+ * dereference pointers to the elements when cxCollectionStoresPointers()
+ * returns true.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @param e pointer to the element
+ */
+#define cx_invoke_destructor_raw(c, e) \
+    if ((c)->collection.simple_destructor) (c)->collection.simple_destructor(e); \
+    if ((c)->collection.advanced_destructor) (c)->collection.advanced_destructor((c)->collection.destructor_data, e)
+
 
 #endif // UCX_COLLECTION_H
--- a/ucx/cx/common.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/common.h	Wed Dec 31 16:40:12 2025 +0100
@@ -133,27 +133,27 @@
 /**
  * Inform the compiler that falling through a switch case is intentional.
  */
-#define cx_attr_fallthrough __attribute__((__fallthrough__))
+#define CX_FALLTHROUGH __attribute__((__fallthrough__))
 
 /**
  * All pointer arguments must be non-NULL.
  */
-#define cx_attr_nonnull __attribute__((__nonnull__))
+#define CX_NONNULL __attribute__((__nonnull__))
 
 /**
  * The specified pointer arguments must be non-NULL.
  */
-#define cx_attr_nonnull_arg(...) __attribute__((__nonnull__(__VA_ARGS__)))
+#define CX_NONNULL_ARG(...) __attribute__((__nonnull__(__VA_ARGS__)))
 
 /**
  * The returned value is guaranteed to be non-NULL.
  */
-#define cx_attr_returns_nonnull __attribute__((__returns_nonnull__))
+#define CX_RETURNS_NONNULL __attribute__((__returns_nonnull__))
 
 /**
  * The attributed function always returns freshly allocated memory.
  */
-#define cx_attr_malloc __attribute__((__malloc__))
+#define CX_MALLOC __attribute__((__malloc__))
 
 #if !defined(__clang__) && __GNUC__ >= 11
 /**
@@ -163,59 +163,59 @@
  * @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) \
+#define CX_DEALLOC(freefunc, freefunc_arg) \
     __attribute__((__malloc__(freefunc, freefunc_arg)))
 #else
 /**
  * Not supported in clang.
  */
-#define cx_attr_dealloc(...)
+#define CX_DEALLOC(...)
 #endif // __clang__
 
 /**
  * Shortcut to specify #cxFree() as deallocator.
  */
-#define cx_attr_dealloc_ucx cx_attr_dealloc(cxFree, 2)
+#define CX_DEALLOC_UCX CX_DEALLOC(cxFree, 2)
 
 /**
  * Specifies the parameters from which the allocation size is calculated.
  */
-#define cx_attr_allocsize(...) __attribute__((__alloc_size__(__VA_ARGS__)))
+#define CX_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)
+#define CX_CSTR_ARG(idx)
 /**
  * No support for the access attribute in clang.
  */
-#define cx_attr_access(mode, ...)
+#define CX_ACCESS(mode, ...)
 #else
 #if __GNUC__ < 10
 /**
  * No support for access attribute in GCC < 10.
  */
-#define cx_attr_access(mode, ...)
+#define CX_ACCESS(mode, ...)
 #else
 /**
  * Helper macro to define access macros.
  */
-#define cx_attr_access(mode, ...) __attribute__((__access__(mode, __VA_ARGS__)))
+#define CX_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)
+#define CX_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) \
+#define CX_CSTR_ARG(idx) \
     __attribute__((__null_terminated_string_arg__(idx)))
 #endif // __GNUC__ < 14
 #endif // __clang__
@@ -227,7 +227,7 @@
  * 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__)
+#define CX_ACCESS_R(...) CX_ACCESS(__read_only__, __VA_ARGS__)
 
 /**
  * Specifies that the function will read and write through the given pointer.
@@ -235,7 +235,7 @@
  * 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__)
+#define CX_ACCESS_RW(...) CX_ACCESS(__read_write__, __VA_ARGS__)
 
 /**
  * Specifies that the function will only write through the given pointer.
@@ -243,27 +243,38 @@
  * 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__)
+#define CX_ACCESS_W(...) CX_ACCESS(__write_only__, __VA_ARGS__)
 
 /**
  * Do not warn about unused variable.
  */
-#define cx_attr_unused __attribute__((__unused__))
+#define CX_UNUSED __attribute__((__unused__))
 
 /**
  * Warn about discarded return value.
  */
-#define cx_attr_nodiscard __attribute__((__warn_unused_result__))
+#define CX_NODISCARD __attribute__((__warn_unused_result__))
 
 
 // ---------------------------------------------------------------------------
-//       MSVC specifics
+//       Support for thread_local
 // ---------------------------------------------------------------------------
 
+#ifdef __cplusplus
+#define cx_thread_local thread_local
+#else // ! __cplusplus
 #ifdef _MSC_VER
-// fix missing _Thread_local support
-#define _Thread_local __declspec(thread)
+#define cx_thread_local __declspec(thread)
+#else // ! _MSC_VER
+#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 202300L
+/** Platform independent thread-local macro. */
+#define cx_thread_local _Thread_local
+#else // C23 or newer
+/** Platform independent thread-local macro. */
+#define cx_thread_local thread_local
+#endif // C23
 #endif // _MSC_VER
+#endif // __cplusplus
 
 // ---------------------------------------------------------------------------
 //       Exported and inlined functions
@@ -278,6 +289,16 @@
 #define CX_EXPORT
 #endif // CX_WINDLL / CX_WINDLL_EXPORT
 
+#ifdef __cplusplus
+#define CX_EXTERN extern "C" CX_EXPORT
+#define CX_FPTR extern "C" typedef
+#else
+/** Declares a function with external linkage. */
+#define CX_EXTERN CX_EXPORT
+/** Defines a function pointer. */
+#define CX_FPTR typedef
+#endif
+
 #ifdef __GNUC__
 /**
  * Declares a function to be inlined.
@@ -355,10 +376,8 @@
  * @retval zero success
  * @retval non-zero the multiplication would overflow
  */
-#if __cplusplus
-extern "C"
-#endif
-CX_EXPORT int cx_szmul_impl(size_t a, size_t b, size_t *result);
+CX_EXTERN
+int cx_szmul_impl(size_t a, size_t b, size_t *result);
 #endif // cx_szmul
 
 #endif // UCX_COMMON_H
--- a/ucx/cx/compare.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/compare.h	Wed Dec 31 16:40:12 2025 +0100
@@ -38,10 +38,6 @@
 
 #include "common.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * A comparator function comparing two arbitrary values.
  *
@@ -54,14 +50,14 @@
  * can be used, but they are NOT compatible with this function
  * pointer.
  */
-typedef int (*cx_compare_func)(const void *left, const void *right);
+CX_FPTR int (*cx_compare_func)(const void *left, const void *right);
 
 /**
  * A comparator function comparing two arbitrary values.
  *
  * Functions with this signature allow specifying a pointer to custom data.
  */
-typedef int (*cx_compare_func2)(const void *left, const void *right, void *data);
+CX_FPTR int (*cx_compare_func2)(const void *left, const void *right, void *data);
 
 /**
  * Compares two integers of type int.
@@ -75,8 +71,8 @@
  * @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
-CX_EXPORT int cx_cmp_int(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_int(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type int.
@@ -87,8 +83,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_int(int i1, int i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_int(int i1, int i2);
 
 /**
  * Compares two integers of type long int.
@@ -102,8 +98,8 @@
  * @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
-CX_EXPORT int cx_cmp_longint(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_longint(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type long int.
@@ -114,8 +110,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_longint(long int i1, long int i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_longint(long int i1, long int i2);
 
 /**
  * Compares two integers of type long long.
@@ -129,8 +125,8 @@
  * @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
-CX_EXPORT int cx_cmp_longlong(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_longlong(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type long long.
@@ -141,8 +137,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_longlong(long long int i1, long long int i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_longlong(long long int i1, long long int i2);
 
 /**
  * Compares two integers of type int16_t.
@@ -156,8 +152,8 @@
  * @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
-CX_EXPORT int cx_cmp_int16(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_int16(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type int16_t.
@@ -168,8 +164,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_int16(int16_t i1, int16_t i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_int16(int16_t i1, int16_t i2);
 
 /**
  * Compares two integers of type int32_t.
@@ -183,8 +179,8 @@
  * @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
-CX_EXPORT int cx_cmp_int32(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_int32(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type int32_t.
@@ -195,8 +191,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_int32(int32_t i1, int32_t i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_int32(int32_t i1, int32_t i2);
 
 /**
  * Compares two integers of type int64_t.
@@ -210,8 +206,8 @@
  * @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
-CX_EXPORT int cx_cmp_int64(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_int64(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type int64_t.
@@ -222,8 +218,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_int64(int64_t i1, int64_t i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_int64(int64_t i1, int64_t i2);
 
 /**
  * Compares two integers of type unsigned int.
@@ -237,8 +233,8 @@
  * @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
-CX_EXPORT int cx_cmp_uint(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_uint(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type unsigned int.
@@ -249,8 +245,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_uint(unsigned int i1, unsigned int i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_uint(unsigned int i1, unsigned int i2);
 
 /**
  * Compares two integers of type unsigned long int.
@@ -264,8 +260,8 @@
  * @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
-CX_EXPORT int cx_cmp_ulongint(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_ulongint(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type unsigned long int.
@@ -276,8 +272,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_ulongint(unsigned long int i1, unsigned long int i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_ulongint(unsigned long int i1, unsigned long int i2);
 
 /**
  * Compares two integers of type unsigned long long.
@@ -291,8 +287,8 @@
  * @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
-CX_EXPORT int cx_cmp_ulonglong(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_ulonglong(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type unsigned long long.
@@ -303,8 +299,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_ulonglong(unsigned long long int i1, unsigned long long int i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_ulonglong(unsigned long long int i1, unsigned long long int i2);
 
 /**
  * Compares two integers of type uint16_t.
@@ -318,8 +314,8 @@
  * @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
-CX_EXPORT int cx_cmp_uint16(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_uint16(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type uint16_t.
@@ -330,8 +326,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_uint16(uint16_t i1, uint16_t i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_uint16(uint16_t i1, uint16_t i2);
 
 /**
  * Compares two integers of type uint32_t.
@@ -345,8 +341,8 @@
  * @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
-CX_EXPORT int cx_cmp_uint32(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_uint32(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type uint32_t.
@@ -357,8 +353,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_uint32(uint32_t i1, uint32_t i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_uint32(uint32_t i1, uint32_t i2);
 
 /**
  * Compares two integers of type uint64_t.
@@ -372,8 +368,8 @@
  * @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
-CX_EXPORT int cx_cmp_uint64(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_uint64(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type uint64_t.
@@ -384,8 +380,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_uint64(uint64_t i1, uint64_t i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_uint64(uint64_t i1, uint64_t i2);
 
 /**
  * Compares two integers of type size_t.
@@ -399,8 +395,8 @@
  * @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
-CX_EXPORT int cx_cmp_size(const void *i1, const void *i2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_size(const void *i1, const void *i2);
 
 /**
  * Compares two integers of type size_t.
@@ -411,8 +407,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_size(size_t i1, size_t i2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_size(size_t i1, size_t i2);
 
 /**
  * Compares two real numbers of type float with precision 1e-6f.
@@ -426,8 +422,8 @@
  * @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
-CX_EXPORT int cx_cmp_float(const void *f1, const void *f2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_float(const void *f1, const void *f2);
 
 /**
  * Compares two real numbers of type float with precision 1e-6f.
@@ -438,8 +434,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_float(float f1, float f2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_float(float f1, float f2);
 
 /**
  * Compares two real numbers of type double with precision 1e-14.
@@ -453,8 +449,8 @@
  * @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
-CX_EXPORT int cx_cmp_double(const void *d1, const void *d2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_double(const void *d1, const void *d2);
 
 /**
  * Compares two real numbers of type double with precision 1e-14.
@@ -465,8 +461,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_double(double d1, double d2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_double(double d1, double d2);
 
 /**
  * Compares the integer representation of two pointers.
@@ -480,8 +476,8 @@
  * @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
-CX_EXPORT int cx_cmp_intptr(const void *ptr1, const void *ptr2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_intptr(const void *ptr1, const void *ptr2);
 
 /**
  * Compares the integer representation of two pointers.
@@ -492,8 +488,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_intptr(intptr_t ptr1, intptr_t ptr2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_intptr(intptr_t ptr1, intptr_t ptr2);
 
 /**
  * Compares the unsigned integer representation of two pointers.
@@ -507,8 +503,8 @@
  * @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
-CX_EXPORT int cx_cmp_uintptr(const void *ptr1, const void *ptr2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_uintptr(const void *ptr1, const void *ptr2);
 
 /**
  * Compares the unsigned integer representation of two pointers.
@@ -519,8 +515,8 @@
  * @retval 0 if both arguments are equal
  * @retval 1 if the left argument is greater than the right argument
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_vcmp_uintptr(uintptr_t ptr1, uintptr_t ptr2);
+CX_EXTERN CX_NODISCARD
+int cx_vcmp_uintptr(uintptr_t ptr1, uintptr_t ptr2);
 
 /**
  * Compares the pointers specified in the arguments without dereferencing.
@@ -531,21 +527,10 @@
  * @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
-CX_EXPORT int cx_cmp_ptr(const void *ptr1, const void *ptr2);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_ptr(const void *ptr1, const void *ptr2);
 
-/**
- * A @c cx_compare_func2 compatible wrapper for @c memcmp().
- *
- * @param ptr1 pointer one
- * @param ptr2 pointer two
- * @param n (@c size_t*) a pointer to the length
- * @return the result of @c memcmp()
- */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT int cx_acmp_memcmp(const void *ptr1, const void *ptr2, void *n);
-
-/** Wraps a compare function for cx_acmp_wrap. */
+/** Wraps a compare function for cx_cmp_wrap. */
 typedef struct {
     /** The wrapped compare function */
     cx_compare_func cmp;
@@ -554,23 +539,13 @@
 /**
  * A @c cx_compare_func2 wrapper for a @c cx_compare_func().
  *
- * This is not strictly compatible with a @c cx_compare_func2 because
- * ISO C does not define conversions between function and object pointers.
- *
- * But it works on all tested platforms to cast a pointer to this function to
- * a @c cx_compare_func2.
- *
  * @param ptr1 pointer one
  * @param ptr2 pointer two
  * @param cmp_wrapper a pointer to a @c cx_compare_func_wrapper
  * @return the result of the invoked compare function
- * @see cx_compare_func_wrapper_s
+ * @see cx_compare_func_wrapper
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT int cx_acmp_wrap(const void *ptr1, const void *ptr2, void* cmp_wrapper);
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cx_cmp_wrap(const void *ptr1, const void *ptr2, void* cmp_wrapper);
 
 #endif //UCX_COMPARE_H
--- a/ucx/cx/hash_key.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/hash_key.h	Wed Dec 31 16:40:12 2025 +0100
@@ -40,10 +40,6 @@
 #include "common.h"
 #include "string.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /** Internal structure for a key within a hash map. */
 struct cx_hash_key_s {
     /**
@@ -78,8 +74,8 @@
  * @param key the key, the hash shall be computed for
  * @see cx_hash_key()
  */
-cx_attr_nonnull
-CX_EXPORT void cx_hash_murmur(CxHashKey *key);
+CX_EXTERN CX_NONNULL
+void cx_hash_murmur(CxHashKey *key);
 
 /**
  * Mixes up a 32-bit integer to be used as a hash.
@@ -89,7 +85,13 @@
  * @param x the integer
  * @return the hash
  */
-CX_EXPORT uint32_t cx_hash_u32(uint32_t x);
+CX_INLINE
+uint32_t cx_hash_u32(uint32_t x) {
+    x = ((x >> 16) ^ x) * 0x45d9f3bu;
+    x = ((x >> 16) ^ x) * 0x45d9f3bu;
+    x = (x >> 16) ^ x;
+    return x;
+}
 
 /**
  * Mixes up a 64-bit integer to be used as a hash.
@@ -99,7 +101,27 @@
  * @param x the integer
  * @return the hash
  */
-CX_EXPORT uint64_t cx_hash_u64(uint64_t x);
+CX_INLINE
+uint64_t cx_hash_u64(uint64_t x){
+    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
+    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
+    x = x ^ (x >> 31);
+    return x;
+}
+
+/**
+ * Computes a hash key for an arbitrary object.
+ *
+ * The computation uses the in-memory representation that might not be
+ * the same on different platforms. Therefore, this hash should not be
+ * used for data exchange with different machines.
+ *
+ * @param obj a pointer to an arbitrary object
+ * @param len the length of the object in memory
+ * @return the hash key
+ */
+CX_EXTERN CX_NODISCARD CX_ACCESS_R(1, 2)
+CxHashKey cx_hash_key(const void *obj, size_t len);
 
 /**
  * Computes a hash key from a 32-bit integer.
@@ -107,8 +129,14 @@
  * @param x the integer
  * @return the hash key
  */
-cx_attr_nodiscard
-CX_EXPORT CxHashKey cx_hash_key_u32(uint32_t x);
+CX_NODISCARD CX_INLINE
+CxHashKey cx_hash_key_u32(uint32_t x) {
+    CxHashKey key;
+    key.data = NULL;
+    key.len = 0;
+    key.hash = cx_hash_u32(x);
+    return key;
+}
 
 /**
  * Computes a hash key from a 64-bit integer.
@@ -116,8 +144,14 @@
  * @param x the integer
  * @return the hash key
  */
-cx_attr_nodiscard
-CX_EXPORT CxHashKey cx_hash_key_u64(uint64_t x);
+CX_NODISCARD CX_INLINE
+CxHashKey cx_hash_key_u64(uint64_t x) {
+    CxHashKey key;
+    key.data = NULL;
+    key.len = 0;
+    key.hash = cx_hash_u64(x);
+    return key;
+}
 
 /**
  * Computes a hash key from a string.
@@ -127,8 +161,10 @@
  * @param str the string
  * @return the hash key
  */
-cx_attr_nodiscard cx_attr_cstr_arg(1)
-CX_EXPORT CxHashKey cx_hash_key_str(const char *str);
+CX_NODISCARD CX_CSTR_ARG(1) CX_INLINE
+CxHashKey cx_hash_key_str(const char *str) {
+    return cx_hash_key((const void*)str, str == NULL ? 0 : strlen(str));
+}
 
 /**
  * Computes a hash key from a string.
@@ -141,8 +177,10 @@
  * @param str the string
  * @return the hash key
  */
-cx_attr_nodiscard cx_attr_cstr_arg(1)
-CX_EXPORT CxHashKey cx_hash_key_ustr(const unsigned char *str);
+CX_NODISCARD CX_CSTR_ARG(1) CX_INLINE
+CxHashKey cx_hash_key_ustr(const unsigned char *str) {
+    return cx_hash_key((const void*)str, str == NULL ? 0 : strlen((const char*)str));
+}
 
 /**
  * Computes a hash key from a byte array.
@@ -151,23 +189,21 @@
  * @param len the length
  * @return the hash key
  */
-cx_attr_nodiscard cx_attr_access_r(1, 2)
-CX_EXPORT CxHashKey cx_hash_key_bytes(const unsigned char *bytes, size_t len);
+CX_NODISCARD CX_ACCESS_R(1, 2) CX_INLINE
+CxHashKey cx_hash_key_bytes(const unsigned char *bytes, size_t len) {
+    return cx_hash_key((const void*)bytes, len);
+}
 
 /**
- * Computes a hash key for an arbitrary object.
+ * Computes a hash key from a UCX string.
  *
- * The computation uses the in-memory representation that might not be
- * the same on different platforms. Therefore, this hash should not be
- * used for data exchange with different machines.
- *
- * @param obj a pointer to an arbitrary object
- * @param len the length of the object in memory
+ * @param str the string
  * @return the hash key
  */
-cx_attr_nodiscard
-cx_attr_access_r(1, 2)
-CX_EXPORT CxHashKey cx_hash_key(const void *obj, size_t len);
+CX_NODISCARD CX_INLINE
+CxHashKey cx_hash_key_cxstr(cxstring str) {
+    return cx_hash_key((void*)str.ptr, str.length);
+}
 
 /**
  * Computes a hash key from a UCX string.
@@ -175,35 +211,40 @@
  * @param str the string
  * @return the hash key
  */
-cx_attr_nodiscard
-CX_EXPORT CxHashKey cx_hash_key_cxstr(cxstring str);
-
-/**
- * Computes a hash key from a UCX string.
- *
- * @param str the string
- * @return the hash key
- */
-cx_attr_nodiscard
-CX_EXPORT CxHashKey cx_hash_key_mutstr(cxmutstr str);
+CX_NODISCARD CX_INLINE
+CxHashKey cx_hash_key_mutstr(cxmutstr str) {
+    return cx_hash_key((void*)str.ptr, str.length);
+}
 
 /**
  * The identity function for the CX_HASH_KEY() macro.
  * You should never need to use this manually.
  *
  * @param key the key
- * @return a copy of the key
+ * @return a copy of the key (not the data)
  */
-cx_attr_nodiscard
-CX_INLINE CxHashKey cx_hash_key_identity(CxHashKey key) {
+CX_NODISCARD CX_INLINE
+CxHashKey cx_hash_key_identity(CxHashKey key) {
     return key;
 }
 
+/**
+ * The dereference function for the CX_HASH_KEY() macro.
+ * You should never need to use this manually.
+ *
+ * @param key a pointer to a key
+ * @return a copy of the key (not the data)
+ */
+CX_NODISCARD CX_INLINE
+CxHashKey cx_hash_key_deref(const CxHashKey *key) {
+    return *key;
+}
+
 #ifndef __cplusplus
 /**
  * Creates a hash key from any of the supported types with implicit length.
  *
- * Does nothing when passing a CxHashkey.
+ * Does nothing when passing a CxHashKey and dereferences CxHashKey pointers.
  *
  * Supported types are UCX strings, zero-terminated C strings,
  * and 32-bit or 64-bit unsigned integers.
@@ -212,6 +253,8 @@
  * @returns the @c CxHashKey
  */
 #define CX_HASH_KEY(key) _Generic((key), \
+        CxHashKey*: cx_hash_key_deref, \
+        const CxHashKey*: cx_hash_key_deref, \
         CxHashKey: cx_hash_key_identity, \
         cxstring: cx_hash_key_cxstr, \
         cxmutstr: cx_hash_key_mutstr, \
@@ -233,12 +276,19 @@
  * @param right (@c CxHashKey*) the second key
  * @return zero when the keys equal, non-zero when they differ
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT int cx_hash_key_cmp(const void *left, const void *right);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+int cx_hash_key_cmp(const void *left, const void *right);
+
+/**
+ * Interprets the key data as a string and returns it.
+ *
+ * @param key the key
+ * @return the key data as a string
+ */
+CX_EXTERN
+cxstring cx_hash_key_as_string(const CxHashKey *key);
 
 #ifdef __cplusplus
-} // extern "C"
-
 // ----------------------------------------------------------
 // Overloads of CX_HASH_KEY (the C++ version of a _Generic)
 // ----------------------------------------------------------
@@ -247,6 +297,10 @@
     return key;
 }
 
+CX_CPPDECL CxHashKey CX_HASH_KEY(const CxHashKey *key) {
+    return *key;
+}
+
 CX_CPPDECL CxHashKey CX_HASH_KEY(cxstring str) {
     return cx_hash_key_cxstr(str);
 }
--- a/ucx/cx/hash_map.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/hash_map.h	Wed Dec 31 16:40:12 2025 +0100
@@ -38,10 +38,6 @@
 
 #include "map.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /** Internal structure for an element of a hash map. */
 struct cx_hash_map_element_s;
 
@@ -83,8 +79,8 @@
  * @param buckets the initial number of buckets in this hash map
  * @return a pointer to the new hash map
  */
-cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxMapFree, 1)
-CX_EXPORT CxMap *cxHashMapCreate(const CxAllocator *allocator,
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxMapFree, 1)
+CxMap *cxHashMapCreate(const CxAllocator *allocator,
         size_t itemsize, size_t buckets);
 
 /**
@@ -106,12 +102,7 @@
  * @retval zero success
  * @retval non-zero if a memory allocation error occurred
  */
-cx_attr_nonnull
-CX_EXPORT int cxMapRehash(CxMap *map);
-
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NONNULL
+int cxMapRehash(CxMap *map);
 
 #endif // UCX_HASH_MAP_H
--- a/ucx/cx/iterator.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/iterator.h	Wed Dec 31 16:40:12 2025 +0100
@@ -38,10 +38,6 @@
 
 #include "common.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * Common data for all iterators.
  */
@@ -183,16 +179,6 @@
 #define cxIteratorFlagRemoval(iter) ((iter).base.remove = (iter).base.allow_remove)
 
 /**
- * Obtains a reference to an arbitrary iterator.
- *
- * This is useful for APIs that expect some iterator as an argument.
- *
- * @param iter the iterator
- * @return (@c struct @c cx_iterator_base_s*) a pointer to the iterator
- */
-#define cxIteratorRef(iter) &((iter).base)
-
-/**
  * Loops over an iterator.
  *
  * @param type the type of the elements
@@ -220,8 +206,8 @@
  * @return an iterator for the specified array
  * @see cxIteratorPtr()
  */
-cx_attr_nodiscard
-CX_EXPORT CxIterator cxIterator(const void *array,
+CX_EXTERN CX_NODISCARD
+CxIterator cxIterator(const void *array,
         size_t elem_size, size_t elem_count);
 
 /**
@@ -237,11 +223,7 @@
  * @return an iterator for the specified array
  * @see cxIterator()
  */
-cx_attr_nodiscard
-CX_EXPORT CxIterator cxIteratorPtr(const void *array, size_t elem_count);
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NODISCARD
+CxIterator cxIteratorPtr(const void *array, size_t elem_count);
 
 #endif // UCX_ITERATOR_H
--- a/ucx/cx/json.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/json.h	Wed Dec 31 16:40:12 2025 +0100
@@ -43,11 +43,6 @@
 #include "array_list.h"
 #include "map.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
 /**
  * The type of the parsed token.
  */
@@ -115,6 +110,10 @@
      */
     CX_JSON_NOTHING, // this allows us to always return non-NULL values
     /**
+     * No meaningful data.
+     */
+    CX_JSON_UNINITIALIZED,
+    /**
      * A JSON object.
      */
     CX_JSON_OBJECT,
@@ -289,13 +288,6 @@
     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.
@@ -310,6 +302,16 @@
     cxmutstr uncompleted_member_name;
 
     /**
+     * Internal buffer for uncompleted tokens.
+     */
+    cxmutstr uncompleted_content;
+
+    /**
+     * The expected type of the currently parsed, uncompleted token.
+     */
+    CxJsonTokenType uncompleted_tokentype;
+
+    /**
      * State stack.
      */
     CX_ARRAY(int, states);
@@ -424,8 +426,8 @@
  *
  * @return new JSON writer settings
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonWriter cxJsonWriterCompact(void);
+CX_EXTERN CX_NODISCARD
+CxJsonWriter cxJsonWriterCompact(void);
 
 /**
  * Creates a default writer configuration for pretty output.
@@ -433,8 +435,8 @@
  * @param use_spaces false if you want tabs, true if you want four spaces instead
  * @return new JSON writer settings
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonWriter cxJsonWriterPretty(bool use_spaces);
+CX_EXTERN CX_NODISCARD
+CxJsonWriter cxJsonWriterPretty(bool use_spaces);
 
 /**
  * Writes a JSON value to a buffer or stream.
@@ -454,8 +456,8 @@
  * @retval zero success
  * @retval non-zero when no or not all data could be written
  */
-cx_attr_nonnull_arg(1, 2, 3)
-CX_EXPORT int cxJsonWrite(void* target, const CxJsonValue* value,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3)
+int cxJsonWrite(void* target, const CxJsonValue* value,
         cx_write_func wfunc, const CxJsonWriter* settings);
 
 
@@ -469,8 +471,8 @@
  * @see cxJsonWriterCompact()
  * @see cxJsonToPrettyString()
  */
-cx_attr_nonnull_arg(2)
-CX_EXPORT cxmutstr cxJsonToString(const CxAllocator *allocator, CxJsonValue *value);
+CX_EXTERN CX_NONNULL_ARG(2) CX_NODISCARD
+cxmutstr cxJsonToString(const CxAllocator *allocator, CxJsonValue *value);
 
 /**
  * Produces a pretty string representation of the specified JSON value.
@@ -482,8 +484,8 @@
  * @see cxJsonWriterPretty()
  * @see cxJsonToString()
  */
-cx_attr_nonnull_arg(2)
-CX_EXPORT cxmutstr cxJsonToPrettyString(const CxAllocator *allocator, CxJsonValue *value);
+CX_EXTERN CX_NONNULL_ARG(2) CX_NODISCARD
+cxmutstr cxJsonToPrettyString(const CxAllocator *allocator, CxJsonValue *value);
 
 /**
  * Initializes the JSON interface.
@@ -492,8 +494,8 @@
  * @param allocator the allocator that shall be used for the produced values
  * @see cxJsonDestroy()
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT void cxJsonInit(CxJson *json, const CxAllocator *allocator);
+CX_EXTERN CX_NONNULL_ARG(1)
+void cxJsonInit(CxJson *json, const CxAllocator *allocator);
 
 /**
  * Destroys the JSON interface.
@@ -501,8 +503,8 @@
  * @param json the JSON interface
  * @see cxJsonInit()
  */
-cx_attr_nonnull
-CX_EXPORT void cxJsonDestroy(CxJson *json);
+CX_EXTERN CX_NONNULL
+void cxJsonDestroy(CxJson *json);
 
 /**
  * Destroys and re-initializes the JSON interface.
@@ -512,8 +514,8 @@
  *
  * @param json the JSON interface
  */
-cx_attr_nonnull
-CX_EXPORT void cxJsonReset(CxJson *json);
+CX_EXTERN CX_NONNULL
+void cxJsonReset(CxJson *json);
 
 /**
  * Fills the input buffer.
@@ -533,8 +535,8 @@
  * @retval non-zero internal allocation error
  * @see cxJsonFill()
  */
-cx_attr_nonnull_arg(1) cx_attr_access_r(2, 3)
-CX_EXPORT int cxJsonFilln(CxJson *json, const char *buf, size_t len);
+CX_EXTERN CX_NONNULL_ARG(1) CX_ACCESS_R(2, 3)
+int cxJsonFilln(CxJson *json, const char *buf, size_t len);
 
 
 /**
@@ -545,8 +547,8 @@
  * @retval zero success
  * @retval non-zero internal allocation error
  */
-cx_attr_nonnull
-CX_INLINE int cx_json_fill(CxJson *json, cxstring str) {
+CX_NONNULL CX_INLINE
+int cx_json_fill(CxJson *json, cxstring str) {
     return cxJsonFilln(json, str.ptr, str.length);
 }
 
@@ -578,8 +580,8 @@
  * @param value a pointer where the JSON value shall be stored to
  * @return status code
  */
-cx_attr_nonnull_arg(3)
-CX_EXPORT CxJsonStatus cx_json_from_string(const CxAllocator *allocator,
+CX_EXTERN CX_NONNULL_ARG(3) CX_ACCESS_W(3)
+CxJsonStatus cx_json_from_string(const CxAllocator *allocator,
             cxstring str, CxJsonValue **value);
 
 /**
@@ -600,6 +602,20 @@
         cx_json_from_string(allocator, cx_strcast(str), value)
 
 /**
+ * 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 a type will be skipped
+ * by the deallocation. 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
+ */
+CX_EXTERN
+void cxJsonValueFree(CxJsonValue *value);
+
+/**
  * Creates a new (empty) JSON object.
  *
  * @param allocator the allocator to use
@@ -607,8 +623,8 @@
  * @see cxJsonObjPutObj()
  * @see cxJsonArrAddValues()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator);
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator);
 
 /**
  * Creates a new (empty) JSON array.
@@ -621,8 +637,8 @@
  * @see cxJsonObjPutArr()
  * @see cxJsonArrAddValues()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator, size_t capacity);
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator, size_t capacity);
 
 /**
  * Creates a new JSON number value.
@@ -633,8 +649,8 @@
  * @see cxJsonObjPutNumber()
  * @see cxJsonArrAddNumbers()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num);
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num);
 
 /**
  * Creates a new JSON number value based on an integer.
@@ -645,8 +661,8 @@
  * @see cxJsonObjPutInteger()
  * @see cxJsonArrAddIntegers()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num);
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num);
 
 /**
  * Creates a new JSON string.
@@ -659,8 +675,8 @@
  * @see cxJsonObjPutString()
  * @see cxJsonArrAddCxStrings()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cx_json_create_string(const CxAllocator* allocator, cxstring str);
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cx_json_create_string(const CxAllocator* allocator, cxstring str);
 
 /**
  * Creates a new JSON string.
@@ -682,8 +698,8 @@
  * @see cxJsonObjPutLiteral()
  * @see cxJsonArrAddLiterals()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit);
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit);
 
 /**
  * Adds number values to a JSON array.
@@ -694,8 +710,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
-CX_EXPORT int cxJsonArrAddNumbers(CxJsonValue* arr, const double* num, size_t count);
+CX_EXTERN CX_NONNULL CX_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.
@@ -706,8 +722,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
-CX_EXPORT int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count);
+CX_EXTERN CX_NONNULL CX_ACCESS_R(2, 3)
+int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count);
 
 /**
  * Adds strings to a JSON array.
@@ -721,8 +737,8 @@
  * @retval non-zero allocation failure
  * @see cxJsonArrAddCxStrings()
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
-CX_EXPORT int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count);
+CX_EXTERN CX_NONNULL CX_ACCESS_R(2, 3)
+int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count);
 
 /**
  * Adds strings to a JSON array.
@@ -736,8 +752,8 @@
  * @retval non-zero allocation failure
  * @see cxJsonArrAddStrings()
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
-CX_EXPORT int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count);
+CX_EXTERN CX_NONNULL CX_ACCESS_R(2, 3)
+int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count);
 
 /**
  * Adds literals to a JSON array.
@@ -748,8 +764,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
-CX_EXPORT int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count);
+CX_EXTERN CX_NONNULL CX_ACCESS_R(2, 3)
+int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count);
 
 /**
  * Add arbitrary values to a JSON array.
@@ -763,8 +779,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
-CX_EXPORT int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count);
+CX_EXTERN CX_NONNULL CX_ACCESS_R(2, 3)
+int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count);
 
 /**
  * Adds or replaces a value within a JSON object.
@@ -777,8 +793,8 @@
  * @retval zero success
  * @retval non-zero allocation failure
  */
-cx_attr_nonnull
-CX_EXPORT int cx_json_obj_put(CxJsonValue* obj, cxstring name, CxJsonValue* child);
+CX_EXTERN CX_NONNULL
+int cx_json_obj_put(CxJsonValue* obj, cxstring name, CxJsonValue* child);
 
 /**
  * Adds or replaces a value within a JSON object.
@@ -807,8 +823,8 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateObj()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue* cx_json_obj_put_obj(CxJsonValue* obj, cxstring name);
+CX_EXTERN CX_NONNULL CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cx_json_obj_put_obj(CxJsonValue* obj, cxstring name);
 
 /**
  * Creates a new JSON object and adds it to an existing object.
@@ -833,8 +849,8 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateArr()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue* cx_json_obj_put_arr(CxJsonValue* obj, cxstring name, size_t capacity);
+CX_EXTERN CX_NONNULL CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cx_json_obj_put_arr(CxJsonValue* obj, cxstring name, size_t capacity);
 
 /**
  * Creates a new JSON array and adds it to an object.
@@ -860,8 +876,8 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateNumber()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue* cx_json_obj_put_number(CxJsonValue* obj, cxstring name, double num);
+CX_EXTERN CX_NONNULL CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cx_json_obj_put_number(CxJsonValue* obj, cxstring name, double num);
 
 /**
  * Creates a new JSON number and adds it to an object.
@@ -887,8 +903,8 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateInteger()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue* cx_json_obj_put_integer(CxJsonValue* obj, cxstring name, int64_t num);
+CX_EXTERN CX_NONNULL CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cx_json_obj_put_integer(CxJsonValue* obj, cxstring name, int64_t num);
 
 /**
  * Creates a new JSON number, based on an integer, and adds it to an object.
@@ -914,8 +930,8 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateString()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue* cx_json_obj_put_string(CxJsonValue* obj, cxstring name, cxstring str);
+CX_EXTERN CX_NONNULL CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cx_json_obj_put_string(CxJsonValue* obj, cxstring name, cxstring str);
 
 /**
  * Creates a new JSON string and adds it to an object.
@@ -943,8 +959,8 @@
  * @see cxJsonObjPut()
  * @see cxJsonCreateLiteral()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue* cx_json_obj_put_literal(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
+CX_EXTERN CX_NONNULL CX_MALLOC CX_DEALLOC(cxJsonValueFree, 1)
+CxJsonValue* cx_json_obj_put_literal(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
 
 /**
  * Creates a new JSON literal and adds it to an object.
@@ -959,19 +975,6 @@
 #define cxJsonObjPutLiteral(obj, name, lit) cx_json_obj_put_literal(obj, cx_strcast(name), 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 a type will be skipped
- * by the deallocation. 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
- */
-CX_EXPORT void cxJsonValueFree(CxJsonValue *value);
-
-/**
  * Tries to obtain the next JSON value.
  *
  * Before this function can be called, the input buffer needs
@@ -993,8 +996,8 @@
  * @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)
-CX_EXPORT CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value);
+CX_EXTERN CX_NONNULL CX_ACCESS_W(2)
+CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value);
 
 /**
  * Checks if the specified value is a JSON object.
@@ -1003,8 +1006,8 @@
  * @retval true the value is a JSON object
  * @retval false otherwise
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsObject(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsObject(const CxJsonValue *value) {
     return value->type == CX_JSON_OBJECT;
 }
 
@@ -1015,8 +1018,8 @@
  * @retval true the value is a JSON array
  * @retval false otherwise
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsArray(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsArray(const CxJsonValue *value) {
     return value->type == CX_JSON_ARRAY;
 }
 
@@ -1027,8 +1030,8 @@
  * @retval true the value is a string
  * @retval false otherwise
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsString(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsString(const CxJsonValue *value) {
     return value->type == CX_JSON_STRING;
 }
 
@@ -1043,8 +1046,8 @@
  * @retval false otherwise
  * @see cxJsonIsInteger()
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsNumber(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsNumber(const CxJsonValue *value) {
     return value->type == CX_JSON_NUMBER || value->type == CX_JSON_INTEGER;
 }
 
@@ -1056,8 +1059,8 @@
  * @retval false otherwise
  * @see cxJsonIsNumber()
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsInteger(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsInteger(const CxJsonValue *value) {
     return value->type == CX_JSON_INTEGER;
 }
 
@@ -1073,8 +1076,8 @@
  * @see cxJsonIsFalse()
  * @see cxJsonIsNull()
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsLiteral(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsLiteral(const CxJsonValue *value) {
     return value->type == CX_JSON_LITERAL;
 }
 
@@ -1087,8 +1090,8 @@
  * @see cxJsonIsTrue()
  * @see cxJsonIsFalse()
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsBool(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsBool(const CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->literal != CX_JSON_NULL;
 }
 
@@ -1104,8 +1107,8 @@
  * @see cxJsonIsBool()
  * @see cxJsonIsFalse()
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsTrue(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsTrue(const CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->literal == CX_JSON_TRUE;
 }
 
@@ -1121,8 +1124,8 @@
  * @see cxJsonIsBool()
  * @see cxJsonIsTrue()
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsFalse(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsFalse(const CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->literal == CX_JSON_FALSE;
 }
 
@@ -1134,8 +1137,8 @@
  * @retval false otherwise
  * @see cxJsonIsLiteral()
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonIsNull(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonIsNull(const CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->literal == CX_JSON_NULL;
 }
 
@@ -1148,8 +1151,8 @@
  * @return the value represented as C string
  * @see cxJsonIsString()
  */
-cx_attr_nonnull  cx_attr_returns_nonnull
-CX_EXPORT char *cxJsonAsString(const CxJsonValue *value);
+CX_EXTERN CX_NONNULL CX_RETURNS_NONNULL CX_NODISCARD
+char *cxJsonAsString(const CxJsonValue *value);
 
 /**
  * Obtains a UCX string from the given JSON value.
@@ -1160,8 +1163,8 @@
  * @return the value represented as UCX string
  * @see cxJsonIsString()
  */
-cx_attr_nonnull
-CX_EXPORT cxstring cxJsonAsCxString(const CxJsonValue *value);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+cxstring cxJsonAsCxString(const CxJsonValue *value);
 
 /**
  * Obtains a mutable UCX string from the given JSON value.
@@ -1172,8 +1175,8 @@
  * @return the value represented as mutable UCX string
  * @see cxJsonIsString()
  */
-cx_attr_nonnull
-CX_EXPORT cxmutstr cxJsonAsCxMutStr(const CxJsonValue *value);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+cxmutstr cxJsonAsCxMutStr(const CxJsonValue *value);
 
 /**
  * Obtains a double-precision floating-point value from the given JSON value.
@@ -1184,8 +1187,8 @@
  * @return the value represented as double
  * @see cxJsonIsNumber()
  */
-cx_attr_nonnull
-CX_EXPORT double cxJsonAsDouble(const CxJsonValue *value);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+double cxJsonAsDouble(const CxJsonValue *value);
 
 /**
  * Obtains a 64-bit signed integer from the given JSON value.
@@ -1199,8 +1202,8 @@
  * @see cxJsonIsNumber()
  * @see cxJsonIsInteger()
  */
-cx_attr_nonnull
-CX_EXPORT int64_t cxJsonAsInteger(const CxJsonValue *value);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int64_t cxJsonAsInteger(const CxJsonValue *value);
 
 /**
  * Obtains a Boolean value from the given JSON value.
@@ -1212,8 +1215,8 @@
  * @return the value represented as double
  * @see cxJsonIsLiteral()
  */
-cx_attr_nonnull
-CX_INLINE bool cxJsonAsBool(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+bool cxJsonAsBool(const CxJsonValue *value) {
     return value->literal == CX_JSON_TRUE;
 }
 
@@ -1226,8 +1229,8 @@
  * @return the size of the array
  * @see cxJsonIsArray()
  */
-cx_attr_nonnull
-CX_INLINE size_t cxJsonArrSize(const CxJsonValue *value) {
+CX_NONNULL CX_NODISCARD CX_INLINE
+size_t cxJsonArrSize(const CxJsonValue *value) {
     return value->array.size;
 }
 
@@ -1245,8 +1248,8 @@
  * @return the value at the specified index
  * @see cxJsonIsArray()
  */
-cx_attr_nonnull cx_attr_returns_nonnull
-CX_EXPORT CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index);
+CX_EXTERN CX_NONNULL CX_RETURNS_NONNULL CX_NODISCARD
+CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index);
 
 /**
  * Removes an element from a JSON array.
@@ -1261,8 +1264,8 @@
  * @return the removed value from the specified index or @c NULL when the index was out of bounds
  * @see cxJsonIsArray()
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue *cxJsonArrRemove(CxJsonValue *value, size_t index);
+CX_EXTERN CX_NONNULL
+CxJsonValue *cxJsonArrRemove(CxJsonValue *value, size_t index);
 
 /**
  * Returns an iterator over the JSON array elements.
@@ -1275,8 +1278,8 @@
  * @return an iterator over the array elements
  * @see cxJsonIsArray()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxIterator cxJsonArrIter(const CxJsonValue *value);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxIterator cxJsonArrIter(const CxJsonValue *value);
 
 /**
  * Returns the size of a JSON object.
@@ -1287,8 +1290,8 @@
  * @return the size of the object, i.e., the number of key/value pairs
  * @see cxJsonIsObject()
  */
-cx_attr_nonnull
-CX_INLINE size_t cxJsonObjSize(const CxJsonValue *value) {
+CX_NONNULL CX_INLINE
+size_t cxJsonObjSize(const CxJsonValue *value) {
     return cxCollectionSize(value->object);
 }
 
@@ -1304,8 +1307,8 @@
  * @return an iterator over the object members
  * @see cxJsonIsObject()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxMapIterator cxJsonObjIter(const CxJsonValue *value);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxMapIterator cxJsonObjIter(const CxJsonValue *value);
 
 /**
  * Internal function, do not use.
@@ -1313,8 +1316,8 @@
  * @param name the key to look up
  * @return the value corresponding to the key
  */
-cx_attr_nonnull cx_attr_returns_nonnull
-CX_EXPORT CxJsonValue *cx_json_obj_get(const CxJsonValue *value, cxstring name);
+CX_EXTERN CX_NONNULL CX_RETURNS_NONNULL CX_NODISCARD
+CxJsonValue *cx_json_obj_get(const CxJsonValue *value, cxstring name);
 
 /**
  * Returns a value corresponding to a key in a JSON object.
@@ -1338,8 +1341,8 @@
  * @param name the key to look up
  * @return the value corresponding to the key or @c NULL when the key is not part of the object
  */
-cx_attr_nonnull
-CX_EXPORT CxJsonValue *cx_json_obj_remove(CxJsonValue *value, cxstring name);
+CX_EXTERN CX_NONNULL
+CxJsonValue *cx_json_obj_remove(CxJsonValue *value, cxstring name);
 
 /**
  * Removes and returns a value corresponding to a key in a JSON object.
@@ -1366,7 +1369,8 @@
  * @retval zero the values are equal (except for ordering of object members)
  * @retval non-zero the values differ
  */
-CX_EXPORT int cxJsonCompare(const CxJsonValue *json, const CxJsonValue *other);
+CX_EXTERN CX_NODISCARD
+int cxJsonCompare(const CxJsonValue *json, const CxJsonValue *other);
 
 
 /**
@@ -1382,11 +1386,10 @@
  * @return the new value or @c NULL if any allocation was unsuccessful
  * @see cxJsonCloneFunc()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cxJsonClone(const CxJsonValue* value,
+CX_EXTERN CX_NODISCARD
+CxJsonValue* cxJsonClone(const CxJsonValue* value,
         const CxAllocator* allocator);
 
-
 /**
  * A @c cx_clone_func compatible version of cxJsonClone().
  *
@@ -1399,8 +1402,8 @@
  * @return the new value or @c NULL if any allocation was unsuccessful
  * @see cxJsonClone()
  */
-cx_attr_nodiscard
-CX_EXPORT CxJsonValue* cx_json_clone_func(
+CX_EXTERN CX_NODISCARD
+CxJsonValue* cx_json_clone_func(
         CxJsonValue* target, const CxJsonValue* source,
         const CxAllocator* allocator, void *data);
 
@@ -1416,9 +1419,5 @@
  */
 #define cxJsonCloneFunc  ((cx_clone_func) cx_json_clone_func)
 
-#ifdef __cplusplus
-}
-#endif
-
 #endif /* UCX_JSON_H */
 
--- a/ucx/cx/kv_list.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/kv_list.h	Wed Dec 31 16:40:12 2025 +0100
@@ -40,10 +40,6 @@
 #include "list.h"
 #include "map.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * Allocates a linked list with a lookup-map for storing elements with @p elem_size bytes each.
  *
@@ -63,8 +59,8 @@
  * @see cxKvListAsMap()
  * @see cxKvListAsList()
  */
-cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxListFree, 1)
-CX_EXPORT CxList *cxKvListCreate(const CxAllocator *allocator,
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxListFree, 1)
+CxList *cxKvListCreate(const CxAllocator *allocator,
         size_t elem_size);
 
 /**
@@ -85,8 +81,8 @@
  * @see cxKvListAsMap()
  * @see cxKvListAsList()
  */
-cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxMapFree, 1)
-CX_EXPORT CxMap *cxKvListCreateAsMap(const CxAllocator *allocator,
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxMapFree, 1)
+CxMap *cxKvListCreateAsMap(const CxAllocator *allocator,
         size_t elem_size);
 
 /**
@@ -95,8 +91,8 @@
  * @param map a map pointer that was returned by a call to cxKvListAsMap()
  * @return the original list pointer
  */
-cx_attr_nodiscard cx_attr_nonnull cx_attr_returns_nonnull
-CX_EXPORT CxList *cxKvListAsList(CxMap *map);
+CX_EXTERN CX_NODISCARD CX_NONNULL CX_RETURNS_NONNULL
+CxList *cxKvListAsList(CxMap *map);
 
 /**
  * Converts a map pointer belonging to a key-value-List back to the original list pointer.
@@ -104,8 +100,8 @@
  * @param list a list created by cxKvListCreate()
  * @return a map pointer that lets you use the list as if it was a map
  */
-cx_attr_nodiscard cx_attr_nonnull cx_attr_returns_nonnull
-CX_EXPORT CxMap *cxKvListAsMap(CxList *list);
+CX_EXTERN CX_NODISCARD CX_NONNULL CX_RETURNS_NONNULL
+CxMap *cxKvListAsMap(CxList *list);
 
 /**
  * Sets or updates the key of a list item.
@@ -120,8 +116,8 @@
  * @retval non-zero memory allocation failure or the index is out of bounds
  * @see cxKvListSetKey()
  */
-cx_attr_nonnull
-CX_EXPORT int cx_kv_list_set_key(CxList *list, size_t index, CxHashKey key);
+CX_EXTERN CX_NONNULL
+int cx_kv_list_set_key(CxList *list, size_t index, CxHashKey key);
 
 /**
  * Inserts an item into the list at the specified index and associates it with the specified key.
@@ -134,8 +130,8 @@
  * @retval non-zero memory allocation failure or the index is out of bounds
  * @see cxKvListInsert()
  */
-cx_attr_nonnull
-CX_EXPORT int cx_kv_list_insert(CxList *list, size_t index, CxHashKey key, void *value);
+CX_EXTERN CX_NONNULL
+int cx_kv_list_insert(CxList *list, size_t index, CxHashKey key, void *value);
 
 /**
  * Sets or updates the key of a list item.
@@ -165,7 +161,6 @@
  */
 #define cxKvListInsert(list, index, key, value) cx_kv_list_insert(list, index, CX_HASH_KEY(key), value)
 
-
 /**
  * Removes the key of a list item.
  *
@@ -178,8 +173,8 @@
  * @retval zero success
  * @retval non-zero the index is out of bounds
  */
-cx_attr_nonnull
-CX_EXPORT int cxKvListRemoveKey(CxList *list, size_t index);
+CX_EXTERN CX_NONNULL
+int cxKvListRemoveKey(CxList *list, size_t index);
 
 /**
  * Returns the key of a list item.
@@ -188,8 +183,8 @@
  * @param index the index of the element in the list
  * @return a pointer to the key or @c NULL when the index is out of bounds or the item does not have a key
  */
-cx_attr_nonnull
-CX_EXPORT const CxHashKey *cxKvListGetKey(CxList *list, size_t index);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+const CxHashKey *cxKvListGetKey(CxList *list, size_t index);
 
 /**
  * Adds an item into the list and associates it with the specified key.
@@ -202,8 +197,4 @@
  */
 #define cxKvListAdd(list, key, value) cxKvListInsert(list, (list)->collection.size, key, value)
 
-#ifdef __cplusplus
-} // extern "C"
-#endif
-
 #endif // UCX_KV_LIST_H
--- a/ucx/cx/linked_list.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/linked_list.h	Wed Dec 31 16:40:12 2025 +0100
@@ -39,10 +39,6 @@
 #include "common.h"
 #include "list.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * Metadata for a linked list.
  */
@@ -94,8 +90,8 @@
  * @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)
-CX_EXPORT CxList *cxLinkedListCreate(const CxAllocator *allocator,
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxListFree, 1)
+CxList *cxLinkedListCreate(const CxAllocator *allocator,
         size_t elem_size);
 
 /**
@@ -112,8 +108,8 @@
  * @param list the list (must be a linked list)
  * @param len the length of the extra data
  */
-cx_attr_nonnull
-CX_EXPORT void cx_linked_list_extra_data(cx_linked_list *list, size_t len);
+CX_EXTERN CX_NONNULL
+void cx_linked_list_extra_data(cx_linked_list *list, size_t len);
 
 /**
  * Finds the node at a certain index.
@@ -132,8 +128,8 @@
  * @param index the search index
  * @return the node found at the specified index
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT void *cx_linked_list_at(const void *start,size_t start_index,
+CX_EXTERN CX_NONNULL CX_NODISCARD
+void *cx_linked_list_at(const void *start,size_t start_index,
         ptrdiff_t loc_advance, size_t index);
 
 /**
@@ -148,8 +144,8 @@
  * @param cmp_func a compare function to compare @p elem against the node data
  * @return a pointer to the found node or @c NULL if no matching node was found
  */
-cx_attr_nonnull_arg(1, 4, 6)
-CX_EXPORT void *cx_linked_list_find(const void *start, ptrdiff_t loc_advance,
+CX_EXTERN CX_NONNULL_ARG(1, 4, 6)
+void *cx_linked_list_find(const void *start, ptrdiff_t loc_advance,
         ptrdiff_t loc_data, const void *elem, size_t *found_index,
         cx_compare_func cmp_func);
 
@@ -166,8 +162,8 @@
  * @param context additional context for the compare function
  * @return a pointer to the found node or @c NULL if no matching node was found
  */
-cx_attr_nonnull_arg(1, 4, 6)
-CX_EXPORT void *cx_linked_list_find_c(const void *start, ptrdiff_t loc_advance,
+CX_EXTERN CX_NONNULL_ARG(1, 4, 6)
+void *cx_linked_list_find_c(const void *start, ptrdiff_t loc_advance,
         ptrdiff_t loc_data, const void *elem, size_t *found_index,
         cx_compare_func2 cmp_func, void *context);
 
@@ -182,8 +178,8 @@
  * @param loc_prev the location of the @c prev pointer
  * @return a pointer to the first node
  */
-cx_attr_nonnull cx_attr_returns_nonnull
-CX_EXPORT void *cx_linked_list_first(const void *node, ptrdiff_t loc_prev);
+CX_EXTERN CX_NONNULL CX_RETURNS_NONNULL
+void *cx_linked_list_first(const void *node, ptrdiff_t loc_prev);
 
 /**
  * Finds the last node in a linked list.
@@ -196,8 +192,8 @@
  * @param loc_next the location of the @c next pointer
  * @return a pointer to the last node
  */
-cx_attr_nonnull cx_attr_returns_nonnull
-CX_EXPORT void *cx_linked_list_last(const void *node, ptrdiff_t loc_next);
+CX_EXTERN CX_NONNULL CX_RETURNS_NONNULL
+void *cx_linked_list_last(const void *node, ptrdiff_t loc_next);
 
 /**
  * Finds the predecessor of a node in case it is not linked.
@@ -209,8 +205,8 @@
  * @param node the successor of the node to find
  * @return the node or @c NULL if @p node has no predecessor
  */
-cx_attr_nonnull
-CX_EXPORT void *cx_linked_list_prev(const void *begin, ptrdiff_t loc_next, const void *node);
+CX_EXTERN CX_NONNULL
+void *cx_linked_list_prev(const void *begin, ptrdiff_t loc_next, const void *node);
 
 /**
  * Adds a new node to a linked list.
@@ -224,8 +220,8 @@
  * @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
  */
-cx_attr_nonnull_arg(5)
-CX_EXPORT void cx_linked_list_add(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node);
+CX_EXTERN CX_NONNULL_ARG(5)
+void cx_linked_list_add(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node);
 
 /**
  * Prepends a new node to a linked list.
@@ -239,8 +235,8 @@
  * @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
  */
-cx_attr_nonnull_arg(5)
-CX_EXPORT void cx_linked_list_prepend(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node);
+CX_EXTERN CX_NONNULL_ARG(5)
+void cx_linked_list_prepend(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node);
 
 /**
  * Links two nodes.
@@ -250,8 +246,8 @@
  * @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)
  */
-cx_attr_nonnull
-CX_EXPORT void cx_linked_list_link(void *left, void *right, ptrdiff_t loc_prev, ptrdiff_t loc_next);
+CX_EXTERN CX_NONNULL
+void cx_linked_list_link(void *left, void *right, ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Unlinks two nodes.
@@ -263,8 +259,8 @@
  * @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)
  */
-cx_attr_nonnull
-CX_EXPORT void cx_linked_list_unlink(void *left, void *right, ptrdiff_t loc_prev, ptrdiff_t loc_next);
+CX_EXTERN CX_NONNULL
+void cx_linked_list_unlink(void *left, void *right, ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
  * Inserts a new node after a given node of a linked list.
@@ -280,8 +276,8 @@
  * @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
  */
-cx_attr_nonnull_arg(6)
-CX_EXPORT void cx_linked_list_insert(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(6)
+void cx_linked_list_insert(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, void *node, void *new_node);
 
 /**
@@ -304,8 +300,8 @@
  * @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)
  */
-cx_attr_nonnull_arg(6)
-CX_EXPORT void cx_linked_list_insert_chain(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(6)
+void cx_linked_list_insert_chain(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, void *node, void *insert_begin, void *insert_end);
 
 /**
@@ -322,8 +318,8 @@
  * @param new_node a pointer to the node that shall be inserted
  * @param cmp_func a compare function that will receive the node pointers
  */
-cx_attr_nonnull_arg(1, 5, 6)
-CX_EXPORT void cx_linked_list_insert_sorted(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(1, 5, 6)
+void cx_linked_list_insert_sorted(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node, cx_compare_func cmp_func);
 
 /**
@@ -345,8 +341,8 @@
  * @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
  */
-cx_attr_nonnull_arg(1, 5, 6)
-CX_EXPORT void cx_linked_list_insert_sorted_chain(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(1, 5, 6)
+void cx_linked_list_insert_sorted_chain(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, void *insert_begin, cx_compare_func cmp_func);
 
 /**
@@ -365,8 +361,8 @@
  * @retval zero when the node was inserted
  * @retval non-zero when a node with the same value already exists
  */
-cx_attr_nonnull_arg(1, 5, 6)
-CX_EXPORT int cx_linked_list_insert_unique(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(1, 5, 6)
+int cx_linked_list_insert_unique(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node, cx_compare_func cmp_func);
 
 /**
@@ -387,11 +383,98 @@
  * @param cmp_func a compare function that will receive the node pointers
  * @return a pointer to a new chain with all duplicates that were not inserted (or @c NULL when there were no duplicates)
  */
-cx_attr_nonnull_arg(1, 5, 6) cx_attr_nodiscard
-CX_EXPORT void *cx_linked_list_insert_unique_chain(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(1, 5, 6) CX_NODISCARD
+void *cx_linked_list_insert_unique_chain(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, void *insert_begin, cx_compare_func cmp_func);
 
 /**
+ * Inserts a node into a sorted linked list.
+ * The new node must not be part of any list yet.
+ *
+ * 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 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 new_node a pointer to the node that shall be inserted
+ * @param cmp_func a compare function that will receive the node pointers
+ * @param context context for the compare function
+ */
+CX_EXTERN CX_NONNULL_ARG(1, 5, 6)
+void cx_linked_list_insert_sorted_c(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node, cx_compare_func2 cmp_func, void *context);
+
+/**
+ * Inserts a chain of nodes into a sorted linked list.
+ * The chain must not be part of any list yet.
+ *
+ * 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
+ * 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
+ * chain may be re-linked with nodes from the target list.
+ *
+ * @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 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
+ * @param context context for the compare function
+ */
+CX_EXTERN CX_NONNULL_ARG(1, 5, 6)
+void cx_linked_list_insert_sorted_chain_c(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *insert_begin, cx_compare_func2 cmp_func, void *context);
+
+/**
+ * Inserts a node into a sorted linked list if no other node with the same value already exists.
+ * The new node must not be part of any list yet.
+ *
+ * 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 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 new_node a pointer to the node that shall be inserted
+ * @param cmp_func a compare function that will receive the node pointers
+ * @param context the context for the compare function
+ * @retval zero when the node was inserted
+ * @retval non-zero when a node with the same value already exists
+ */
+CX_EXTERN CX_NONNULL_ARG(1, 5, 6)
+int cx_linked_list_insert_unique_c(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *new_node, cx_compare_func2 cmp_func, void *context);
+
+/**
+ * Inserts a chain of nodes into a sorted linked list, avoiding duplicates.
+ * The chain must not be part of any list yet.
+ *
+ * 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_sorted(), not all nodes of the
+ * chain might be added. This function returns a new chain consisting of all the duplicates.
+ *
+ * @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 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
+ * @param context context for the compare function
+ * @return a pointer to a new chain with all duplicates that were not inserted (or @c NULL when there were no duplicates)
+ */
+CX_EXTERN CX_NONNULL_ARG(1, 5, 6) CX_NODISCARD
+void *cx_linked_list_insert_unique_chain_c(void **begin, void **end,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next, void *insert_begin, cx_compare_func2 cmp_func, void *context);
+
+/**
  * Removes a chain of nodes from the linked list.
  *
  * If one of the nodes to remove is the beginning (resp. end) node of the list, and if @p begin (resp. @p end)
@@ -412,8 +495,8 @@
  * @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)
-CX_EXPORT size_t cx_linked_list_remove_chain(void **begin, void **end,
+CX_EXTERN CX_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);
 
 /**
@@ -435,8 +518,8 @@
  * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param node the node to remove
  */
-cx_attr_nonnull_arg(5)
-CX_EXPORT void cx_linked_list_remove(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(5)
+void cx_linked_list_remove(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, void *node);
 
 /**
@@ -446,8 +529,8 @@
  * @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
  */
-cx_attr_nodiscard
-CX_EXPORT size_t cx_linked_list_size(const void *node, ptrdiff_t loc_next);
+CX_EXTERN CX_NODISCARD
+size_t cx_linked_list_size(const void *node, ptrdiff_t loc_next);
 
 /**
  * Sorts a linked list based on a comparison function.
@@ -461,8 +544,8 @@
  * @param loc_data the location of the @c data pointer within your node struct
  * @param cmp_func the compare function defining the sort order
  */
-cx_attr_nonnull_arg(1, 6)
-CX_EXPORT void cx_linked_list_sort(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(1, 6)
+void cx_linked_list_sort(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, ptrdiff_t loc_data, cx_compare_func cmp_func);
 
 /**
@@ -478,11 +561,10 @@
  * @param cmp_func the compare function defining the sort order
  * @param context additional context for the compare function
  */
-cx_attr_nonnull_arg(1, 6)
-CX_EXPORT void cx_linked_list_sort_c(void **begin, void **end,
+CX_EXTERN CX_NONNULL_ARG(1, 6)
+void cx_linked_list_sort_c(void **begin, void **end,
         ptrdiff_t loc_prev, ptrdiff_t loc_next, ptrdiff_t loc_data, cx_compare_func2 cmp_func, void *context);
 
-
 /**
  * Compares two lists element wise.
  *
@@ -496,8 +578,8 @@
  * @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.
  */
-cx_attr_nonnull_arg(5)
-CX_EXPORT int cx_linked_list_compare(const void *begin_left, const void *begin_right,
+CX_EXTERN CX_NONNULL_ARG(5)
+int cx_linked_list_compare(const void *begin_left, const void *begin_right,
         ptrdiff_t loc_advance, ptrdiff_t loc_data, cx_compare_func cmp_func);
 
 /**
@@ -510,11 +592,12 @@
  * @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 the function to compare the elements
+ * @param context the context for the compare function
  * @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.
  */
-cx_attr_nonnull_arg(5)
-CX_EXPORT int cx_linked_list_compare_c(const void *begin_left, const void *begin_right,
+CX_EXTERN CX_NONNULL_ARG(5)
+int cx_linked_list_compare_c(const void *begin_left, const void *begin_right,
         ptrdiff_t loc_advance, ptrdiff_t loc_data, cx_compare_func2 cmp_func, void *context);
 
 /**
@@ -525,11 +608,7 @@
  * @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)
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT void cx_linked_list_reverse(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next);
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NONNULL_ARG(1)
+void cx_linked_list_reverse(void **begin, void **end, ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 #endif // UCX_LINKED_LIST_H
--- a/ucx/cx/list.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/list.h	Wed Dec 31 16:40:12 2025 +0100
@@ -39,10 +39,6 @@
 #include "common.h"
 #include "collection.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * List class type.
  */
@@ -206,8 +202,8 @@
  * @param n the number of elements to insert
  * @return the number of elements actually inserted
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_list_default_insert_array(struct cx_list_s *list,
+CX_EXTERN CX_NONNULL_ARG(1)
+size_t cx_list_default_insert_array(struct cx_list_s *list,
         size_t index, const void *data, size_t n);
 
 /**
@@ -226,8 +222,8 @@
  * @param n the number of elements to insert
  * @return the number of elements actually inserted
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_list_default_insert_sorted(struct cx_list_s *list,
+CX_EXTERN CX_NONNULL
+size_t cx_list_default_insert_sorted(struct cx_list_s *list,
         const void *sorted_data, size_t n);
 
 /**
@@ -246,8 +242,8 @@
  * @param n the number of elements to insert
  * @return the number of elements from the @p sorted_data that are definitely present in the list after this call
  */
-cx_attr_nonnull
-CX_EXPORT size_t cx_list_default_insert_unique(struct cx_list_s *list,
+CX_EXTERN CX_NONNULL
+size_t cx_list_default_insert_unique(struct cx_list_s *list,
         const void *sorted_data, size_t n);
 
 /**
@@ -261,8 +257,8 @@
  *
  * @param list the list that shall be sorted
  */
-cx_attr_nonnull
-CX_EXPORT void cx_list_default_sort(struct cx_list_s *list);
+CX_EXTERN CX_NONNULL
+void cx_list_default_sort(struct cx_list_s *list);
 
 /**
  * Default unoptimized swap implementation.
@@ -277,8 +273,8 @@
  * @retval non-zero when indices are out of bounds or memory
  * allocation for the temporary buffer fails
  */
-cx_attr_nonnull
-CX_EXPORT int cx_list_default_swap(struct cx_list_s *list, size_t i, size_t j);
+CX_EXTERN CX_NONNULL
+int cx_list_default_swap(struct cx_list_s *list, size_t i, size_t j);
 
 /**
  * Initializes a list struct.
@@ -287,11 +283,8 @@
  * The purpose of this function is to be called in the initialization code
  * of your list to set certain members correctly.
  *
- * This is particularly important when you want your list to support
- * #CX_STORE_POINTERS as @p elem_size. This function will wrap the list
- * class accordingly and make sure that you can implement your list as if
- * it was only storing objects, and the wrapper will automatically enable
- * the feature of storing pointers.
+ * This is particularly useful when you want your list to support
+ * #CX_STORE_POINTERS as @p elem_size.
  *
  * @par Example
  *
@@ -322,8 +315,8 @@
  * @param allocator the allocator for the elements
  * @param elem_size the size of one element
  */
-cx_attr_nonnull_arg(1, 2, 3)
-CX_EXPORT void cx_list_init(struct cx_list_s *list,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3)
+void cx_list_init(struct cx_list_s *list,
     struct cx_list_class_s *cl, const struct cx_allocator_s *allocator,
     size_t elem_size);
 
@@ -335,8 +328,8 @@
  * @param list the list which is comparing the elements
  * @return the comparison result
  */
-cx_attr_nonnull
-CX_EXPORT int cx_list_compare_wrapper(
+CX_EXTERN CX_NONNULL
+int cx_list_compare_wrapper(
     const void *left, const void *right, void *list);
 
 /**
@@ -345,8 +338,8 @@
  * @param list the list
  * @return the number of currently stored elements
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxListSize(const CxList *list);
+CX_EXTERN CX_NONNULL
+size_t cxListSize(const CxList *list);
 
 /**
  * Adds an item to the end of the list.
@@ -358,8 +351,8 @@
  * @see cxListAddArray()
  * @see cxListEmplace()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListAdd(CxList *list, const void *elem);
+CX_EXTERN CX_NONNULL
+int cxListAdd(CxList *list, const void *elem);
 
 /**
  * Adds multiple items to the end of the list.
@@ -378,8 +371,8 @@
  * @return the number of added elements
  * @see cxListEmplaceArray()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxListAddArray(CxList *list, const void *array, size_t n);
+CX_EXTERN CX_NONNULL
+size_t cxListAddArray(CxList *list, const void *array, size_t n);
 
 /**
  * Inserts an item at the specified index.
@@ -395,8 +388,8 @@
  * @see cxListInsertBefore()
  * @see cxListEmplaceAt()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListInsert(CxList *list, size_t index, const void *elem);
+CX_EXTERN CX_NONNULL
+int cxListInsert(CxList *list, size_t index, const void *elem);
 
 /**
  * Allocates memory for an element at the specified index and returns a pointer to that memory.
@@ -410,8 +403,8 @@
  * @see cxListEmplaceArrayAt()
  * @see cxListInsert()
  */
-cx_attr_nonnull
-CX_EXPORT void *cxListEmplaceAt(CxList *list, size_t index);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+void *cxListEmplaceAt(CxList *list, size_t index);
 
 /**
  * Allocates memory for an element at the end of the list and returns a pointer to that memory.
@@ -423,8 +416,8 @@
  * @see cxListEmplaceAt()
  * @see cxListAdd()
  */
-cx_attr_nonnull
-CX_EXPORT void *cxListEmplace(CxList *list);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+void *cxListEmplace(CxList *list);
 
 /**
  * Allocates memory for multiple elements and returns an iterator.
@@ -443,8 +436,8 @@
  * @see cxListEmplaceAt()
  * @see cxListInsertArray()
  */
-cx_attr_nonnull
-CX_EXPORT CxIterator cxListEmplaceArrayAt(CxList *list, size_t index, size_t n);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxIterator cxListEmplaceArrayAt(CxList *list, size_t index, size_t n);
 
 /**
  * Allocates memory for multiple elements and returns an iterator.
@@ -462,8 +455,8 @@
  * @see cxListEmplace()
  * @see cxListAddArray()
  */
-cx_attr_nonnull
-CX_EXPORT CxIterator cxListEmplaceArray(CxList *list, size_t n);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxIterator cxListEmplaceArray(CxList *list, size_t n);
 
 /**
  * Inserts an item into a sorted list.
@@ -475,8 +468,8 @@
  * @retval zero success
  * @retval non-zero memory allocation failure
  */
-cx_attr_nonnull
-CX_EXPORT int cxListInsertSorted(CxList *list, const void *elem);
+CX_EXTERN CX_NONNULL
+int cxListInsertSorted(CxList *list, const void *elem);
 
 /**
  * Inserts an item into a list if it does not exist.
@@ -491,8 +484,8 @@
  * @retval zero success (also when the element was already in the list)
  * @retval non-zero memory allocation failure
  */
-cx_attr_nonnull
-CX_EXPORT int cxListInsertUnique(CxList *list, const void *elem);
+CX_EXTERN CX_NONNULL
+int cxListInsertUnique(CxList *list, const void *elem);
 
 /**
  * Inserts multiple items to the list at the specified index.
@@ -514,8 +507,8 @@
  * @return the number of added elements
  * @see cxListEmplaceArrayAt()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxListInsertArray(CxList *list, size_t index, const void *array, size_t n);
+CX_EXTERN CX_NONNULL
+size_t cxListInsertArray(CxList *list, size_t index, const void *array, size_t n);
 
 /**
  * Inserts a sorted array into a sorted list.
@@ -536,8 +529,8 @@
  * @param n the number of elements to add
  * @return the number of added elements
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxListInsertSortedArray(CxList *list, const void *array, size_t n);
+CX_EXTERN CX_NONNULL
+size_t cxListInsertSortedArray(CxList *list, const void *array, size_t n);
 
 /**
  * Inserts an array into a list, skipping duplicates.
@@ -571,8 +564,8 @@
  *
  * @return the number of elements from the @p sorted_data that are definitely present in the list after this call
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxListInsertUniqueArray(CxList *list, const void *array, size_t n);
+CX_EXTERN CX_NONNULL
+size_t cxListInsertUniqueArray(CxList *list, const void *array, size_t n);
 
 /**
  * Inserts an element after the current location of the specified iterator.
@@ -590,8 +583,8 @@
  * @see cxListInsert()
  * @see cxListInsertBefore()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListInsertAfter(CxIterator *iter, const void *elem);
+CX_EXTERN CX_NONNULL
+int cxListInsertAfter(CxIterator *iter, const void *elem);
 
 /**
  * Inserts an element before the current location of the specified iterator.
@@ -609,8 +602,8 @@
  * @see cxListInsert()
  * @see cxListInsertAfter()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListInsertBefore(CxIterator *iter, const void *elem);
+CX_EXTERN CX_NONNULL
+int cxListInsertBefore(CxIterator *iter, const void *elem);
 
 /**
  * Removes the element at the specified index.
@@ -623,8 +616,8 @@
  * @retval zero success
  * @retval non-zero index out of bounds
  */
-cx_attr_nonnull
-CX_EXPORT int cxListRemove(CxList *list, size_t index);
+CX_EXTERN CX_NONNULL
+int cxListRemove(CxList *list, size_t index);
 
 /**
  * Removes and returns the element at the specified index.
@@ -639,8 +632,8 @@
  * @retval zero success
  * @retval non-zero index out of bounds
  */
-cx_attr_nonnull cx_attr_access_w(3)
-CX_EXPORT int cxListRemoveAndGet(CxList *list, size_t index, void *targetbuf);
+CX_EXTERN CX_NONNULL CX_ACCESS_W(3)
+int cxListRemoveAndGet(CxList *list, size_t index, void *targetbuf);
 
 /**
  * Removes and returns the first element of the list.
@@ -656,8 +649,8 @@
  * @see cxListPopFront()
  * @see cxListRemoveAndGetLast()
  */
-cx_attr_nonnull cx_attr_access_w(2)
-CX_EXPORT int cxListRemoveAndGetFirst(CxList *list, void *targetbuf);
+CX_EXTERN CX_NONNULL CX_ACCESS_W(2)
+int cxListRemoveAndGetFirst(CxList *list, void *targetbuf);
 
 /**
  * Removes and returns the first element of the list.
@@ -690,8 +683,8 @@
  * @retval zero success
  * @retval non-zero the list is empty
  */
-cx_attr_nonnull cx_attr_access_w(2)
-CX_EXPORT int cxListRemoveAndGetLast(CxList *list, void *targetbuf);
+CX_EXTERN CX_NONNULL CX_ACCESS_W(2)
+int cxListRemoveAndGetLast(CxList *list, void *targetbuf);
 
 /**
  * Removes and returns the last element of the list.
@@ -726,8 +719,8 @@
  * @param num the number of elements to remove
  * @return the actual number of removed elements
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxListRemoveArray(CxList *list, size_t index, size_t num);
+CX_EXTERN CX_NONNULL
+size_t cxListRemoveArray(CxList *list, size_t index, size_t num);
 
 /**
  * Removes and returns multiple elements starting at the specified index.
@@ -742,8 +735,8 @@
  * @param targetbuf a buffer where to copy the elements
  * @return the actual number of removed elements
  */
-cx_attr_nonnull cx_attr_access_w(4)
-CX_EXPORT size_t cxListRemoveArrayAndGet(CxList *list, size_t index, size_t num, void *targetbuf);
+CX_EXTERN CX_NONNULL CX_ACCESS_W(4)
+size_t cxListRemoveArrayAndGet(CxList *list, size_t index, size_t num, void *targetbuf);
 
 /**
  * Removes all elements from this list.
@@ -753,8 +746,8 @@
  *
  * @param list the list
  */
-cx_attr_nonnull
-CX_EXPORT void cxListClear(CxList *list);
+CX_EXTERN CX_NONNULL
+void cxListClear(CxList *list);
 
 /**
  * Swaps two items in the list.
@@ -769,8 +762,8 @@
  * @retval non-zero one of the indices is out of bounds,
  * or the swap needed extra memory, but allocation failed
  */
-cx_attr_nonnull
-CX_EXPORT int cxListSwap(CxList *list, size_t i, size_t j);
+CX_EXTERN CX_NONNULL
+int cxListSwap(CxList *list, size_t i, size_t j);
 
 /**
  * Returns a pointer to the element at the specified index.
@@ -781,8 +774,8 @@
  * @param index the index of the element
  * @return a pointer to the element or @c NULL if the index is out of bounds
  */
-cx_attr_nonnull
-CX_EXPORT void *cxListAt(const CxList *list, size_t index);
+CX_EXTERN CX_NONNULL
+void *cxListAt(const CxList *list, size_t index);
 
 /**
  * Returns a pointer to the first element.
@@ -792,8 +785,8 @@
  * @param list the list
  * @return a pointer to the first element or @c NULL if the list is empty
  */
-cx_attr_nonnull
-CX_EXPORT void *cxListFirst(const CxList *list);
+CX_EXTERN CX_NONNULL
+void *cxListFirst(const CxList *list);
 
 /**
  * Returns a pointer to the last element.
@@ -803,8 +796,8 @@
  * @param list the list
  * @return a pointer to the last element or @c NULL if the list is empty
  */
-cx_attr_nonnull
-CX_EXPORT void *cxListLast(const CxList *list);
+CX_EXTERN CX_NONNULL
+void *cxListLast(const CxList *list);
 
 /**
  * Sets the element at the specified index in the list.
@@ -818,8 +811,8 @@
  * @retval zero on success
  * @retval non-zero when index is out of bounds
  */
-cx_attr_nonnull
-CX_EXPORT int cxListSet(CxList *list, size_t index, const void *elem);
+CX_EXTERN CX_NONNULL
+int cxListSet(CxList *list, size_t index, const void *elem);
 
 /**
  * Returns an iterator pointing to the item at the specified index.
@@ -832,8 +825,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-cx_attr_nodiscard
-CX_EXPORT CxIterator cxListIteratorAt(const CxList *list, size_t index);
+CX_EXTERN CX_NODISCARD
+CxIterator cxListIteratorAt(const CxList *list, size_t index);
 
 /**
  * Returns a backwards iterator pointing to the item at the specified index.
@@ -846,8 +839,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-cx_attr_nodiscard
-CX_EXPORT CxIterator cxListBackwardsIteratorAt(const CxList *list, size_t index);
+CX_EXTERN CX_NODISCARD
+CxIterator cxListBackwardsIteratorAt(const CxList *list, size_t index);
 
 /**
  * Returns an iterator pointing to the first item of the list.
@@ -859,8 +852,8 @@
  * @param list the list
  * @return a new iterator
  */
-cx_attr_nodiscard
-CX_EXPORT CxIterator cxListIterator(const CxList *list);
+CX_EXTERN CX_NODISCARD
+CxIterator cxListIterator(const CxList *list);
 
 /**
  * Returns a backwards iterator pointing to the last item of the list.
@@ -872,8 +865,8 @@
  * @param list the list
  * @return a new iterator
  */
-cx_attr_nodiscard
-CX_EXPORT CxIterator cxListBackwardsIterator(const CxList *list);
+CX_EXTERN CX_NODISCARD
+CxIterator cxListBackwardsIterator(const CxList *list);
 
 /**
  * Returns the index of the first element that equals @p elem.
@@ -886,8 +879,8 @@
  * @see cxListIndexValid()
  * @see cxListContains()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT size_t cxListFind(const CxList *list, const void *elem);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+size_t cxListFind(const CxList *list, const void *elem);
 
 /**
  * Checks if the list contains the specified element.
@@ -900,8 +893,8 @@
  * @retval false if the element is not contained
  * @see cxListFind()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT bool cxListContains(const CxList* list, const void* elem);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+bool cxListContains(const CxList* list, const void* elem);
 
 /**
  * Checks if the specified index is within bounds.
@@ -911,8 +904,8 @@
  * @retval true if the index is within bounds
  * @retval false if the index is out of bounds
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT bool cxListIndexValid(const CxList *list, size_t index);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+bool cxListIndexValid(const CxList *list, size_t index);
 
 /**
  * Removes and returns the index of the first element that equals @p elem.
@@ -925,8 +918,8 @@
  * when the element is not found or could not be removed
  * @see cxListIndexValid()
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxListFindRemove(CxList *list, const void *elem);
+CX_EXTERN CX_NONNULL
+size_t cxListFindRemove(CxList *list, const void *elem);
 
 /**
  * Sorts the list.
@@ -935,16 +928,16 @@
  *
  * @param list the list
  */
-cx_attr_nonnull
-CX_EXPORT void cxListSort(CxList *list);
+CX_EXTERN CX_NONNULL
+void cxListSort(CxList *list);
 
 /**
  * Reverses the order of the items.
  *
  * @param list the list
  */
-cx_attr_nonnull
-CX_EXPORT void cxListReverse(CxList *list);
+CX_EXTERN CX_NONNULL
+void cxListReverse(CxList *list);
 
 /**
  * Compares a list to another list of the same type.
@@ -960,8 +953,8 @@
  * @retval positive the first list is larger
  * or the first non-equal element in the first list is larger
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT int cxListCompare(const CxList *list, const CxList *other);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+int cxListCompare(const CxList *list, const CxList *other);
 
 /**
  * Deallocates the memory of the specified list structure.
@@ -970,7 +963,8 @@
  *
  * @param list the list that shall be freed
  */
-CX_EXPORT void cxListFree(CxList *list);
+CX_EXTERN
+void cxListFree(CxList *list);
 
 
 /**
@@ -992,8 +986,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListCloneShallow()
  */
-cx_attr_nonnull_arg(1, 2, 3)
-CX_EXPORT int cxListClone(CxList *dst, const CxList *src,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3)
+int cxListClone(CxList *dst, const CxList *src,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 /**
@@ -1015,8 +1009,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListDifferenceShallow()
  */
-cx_attr_nonnull_arg(1, 2, 3, 4)
-CX_EXPORT int cxListDifference(CxList *dst,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4)
+int cxListDifference(CxList *dst,
         const CxList *minuend, const CxList *subtrahend,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
@@ -1039,8 +1033,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListIntersectionShallow()
  */
-cx_attr_nonnull_arg(1, 2, 3, 4)
-CX_EXPORT int cxListIntersection(CxList *dst, const CxList *src, const CxList *other,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4)
+int cxListIntersection(CxList *dst, const CxList *src, const CxList *other,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 /**
@@ -1064,8 +1058,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListUnionShallow()
  */
-cx_attr_nonnull_arg(1, 2, 3, 4)
-CX_EXPORT int cxListUnion(CxList *dst, const CxList *src, const CxList *other,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4)
+int cxListUnion(CxList *dst, const CxList *src, const CxList *other,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 /**
@@ -1087,8 +1081,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListClone()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListCloneShallow(CxList *dst, const CxList *src);
+CX_EXTERN CX_NONNULL
+int cxListCloneShallow(CxList *dst, const CxList *src);
 
 /**
  * Clones elements from a list only if they are not present in another list.
@@ -1109,8 +1103,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListDifference()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListDifferenceShallow(CxList *dst,
+CX_EXTERN CX_NONNULL
+int cxListDifferenceShallow(CxList *dst,
         const CxList *minuend, const CxList *subtrahend);
 
 /**
@@ -1132,8 +1126,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListIntersection()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListIntersectionShallow(CxList *dst, const CxList *src, const CxList *other);
+CX_EXTERN CX_NONNULL
+int cxListIntersectionShallow(CxList *dst, const CxList *src, const CxList *other);
 
 /**
  * Performs a deep clone of one list into another, skipping duplicates.
@@ -1156,8 +1150,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListUnion()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListUnionShallow(CxList *dst, const CxList *src, const CxList *other);
+CX_EXTERN CX_NONNULL
+int cxListUnionShallow(CxList *dst, const CxList *src, const CxList *other);
 
 /**
  * Asks the list to reserve enough memory for a given total number of elements.
@@ -1176,8 +1170,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxListShrink()
  */
-cx_attr_nonnull
-CX_EXPORT int cxListReserve(CxList *list, size_t capacity);
+CX_EXTERN CX_NONNULL
+int cxListReserve(CxList *list, size_t capacity);
 
 /**
  * Advises the list to free any overallocated memory.
@@ -1190,11 +1184,7 @@
  * @param list the list
  * @return usually zero
  */
-cx_attr_nonnull
-CX_EXPORT int cxListShrink(CxList *list);
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NONNULL
+int cxListShrink(CxList *list);
 
 #endif // UCX_LIST_H
--- a/ucx/cx/map.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/map.h	Wed Dec 31 16:40:12 2025 +0100
@@ -46,10 +46,6 @@
 typedef struct cx_list_s CxList;
 #endif
 
-#ifdef    __cplusplus
-extern "C" {
-#endif
-
 /** Type for the UCX map. */
 typedef struct cx_map_s CxMap;
 
@@ -227,8 +223,8 @@
  *
  * @param map the map to be freed
  */
-CX_EXPORT void cxMapFree(CxMap *map);
-
+CX_EXTERN
+void cxMapFree(CxMap *map);
 
 /**
  * Clears a map by removing all elements.
@@ -237,8 +233,8 @@
  *
  * @param map the map to be cleared
  */
-cx_attr_nonnull
-CX_EXPORT void cxMapClear(CxMap *map);
+CX_EXTERN CX_NONNULL
+void cxMapClear(CxMap *map);
 
 /**
  * Returns the number of elements in this map.
@@ -246,8 +242,8 @@
  * @param map the map
  * @return the number of stored elements
  */
-cx_attr_nonnull
-CX_EXPORT size_t cxMapSize(const CxMap *map);
+CX_EXTERN CX_NONNULL
+size_t cxMapSize(const CxMap *map);
 
 /**
  * Creates a value iterator for a map.
@@ -262,8 +258,8 @@
  * @param map the map to create the iterator for (can be @c NULL)
  * @return an iterator for the currently stored values
  */
-cx_attr_nodiscard
-CX_EXPORT CxMapIterator cxMapIteratorValues(const CxMap *map);
+CX_EXTERN CX_NODISCARD
+CxMapIterator cxMapIteratorValues(const CxMap *map);
 
 /**
  * Creates a key iterator for a map.
@@ -277,8 +273,8 @@
  * @param map the map to create the iterator for (can be @c NULL)
  * @return an iterator for the currently stored keys
  */
-cx_attr_nodiscard
-CX_EXPORT CxMapIterator cxMapIteratorKeys(const CxMap *map);
+CX_EXTERN CX_NODISCARD
+CxMapIterator cxMapIteratorKeys(const CxMap *map);
 
 /**
  * Creates an iterator for a map.
@@ -294,8 +290,8 @@
  * @see cxMapIteratorKeys()
  * @see cxMapIteratorValues()
  */
-cx_attr_nodiscard
-CX_EXPORT CxMapIterator cxMapIterator(const CxMap *map);
+CX_EXTERN CX_NODISCARD
+CxMapIterator cxMapIterator(const CxMap *map);
 
 /**
  * Puts a key/value-pair into the map.
@@ -317,8 +313,8 @@
  * @retval non-zero value on memory allocation failure
  * @see cxMapPut()
  */
-cx_attr_nonnull
-CX_EXPORT int cx_map_put(CxMap *map, CxHashKey key, void *value);
+CX_EXTERN CX_NONNULL
+int cx_map_put(CxMap *map, CxHashKey key, void *value);
 
 /**
  * Puts a key/value-pair into the map.
@@ -359,8 +355,8 @@
  * @return the pointer to the allocated memory or @c NULL if allocation fails
  * @see cxMapEmplace()
  */
-cx_attr_nonnull
-CX_EXPORT void *cx_map_emplace(CxMap *map, CxHashKey key);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+void *cx_map_emplace(CxMap *map, CxHashKey key);
 
 /**
  * Allocates memory for a value in the map associated with the specified key.
@@ -393,8 +389,8 @@
  * @return the value
  * @see cxMapGet()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT void *cx_map_get(const CxMap *map, CxHashKey key);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+void *cx_map_get(const CxMap *map, CxHashKey key);
 
 /**
  * Retrieves a value by using a key.
@@ -436,8 +432,8 @@
  * @see cxMapRemove()
  * @see cxMapRemoveAndGet()
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT int cx_map_remove(CxMap *map, CxHashKey key, void *targetbuf);
+CX_EXTERN CX_NONNULL_ARG(1)
+int cx_map_remove(CxMap *map, CxHashKey key, void *targetbuf);
 
 /**
  * Removes a key/value-pair from the map by using the key.
@@ -476,7 +472,6 @@
  */
 #define cxMapRemoveAndGet(map, key, targetbuf) cx_map_remove(map, CX_HASH_KEY(key), targetbuf)
 
-
 /**
  * Performs a deep clone of one map into another.
  *
@@ -499,11 +494,10 @@
  * @retval zero when all elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull_arg(1, 2, 3)
-CX_EXPORT int cxMapClone(CxMap *dst, const CxMap *src,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3)
+int cxMapClone(CxMap *dst, const CxMap *src,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
-
 /**
  * Clones entries of a map if their key is not present in another map.
  *
@@ -516,8 +510,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull_arg(1, 2, 3, 4)
-CX_EXPORT int cxMapDifference(CxMap *dst, const CxMap *minuend, const CxMap *subtrahend,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4)
+int cxMapDifference(CxMap *dst, const CxMap *minuend, const CxMap *subtrahend,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 /**
@@ -537,8 +531,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull_arg(1, 2, 3, 4)
-CX_EXPORT int cxMapListDifference(CxMap *dst, const CxMap *src, const CxList *keys,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4)
+int cxMapListDifference(CxMap *dst, const CxMap *src, const CxList *keys,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 
@@ -554,8 +548,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull_arg(1, 2, 3, 4)
-CX_EXPORT int cxMapIntersection(CxMap *dst, const CxMap *src, const CxMap *other,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4)
+int cxMapIntersection(CxMap *dst, const CxMap *src, const CxMap *other,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 /**
@@ -575,8 +569,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull_arg(1, 2, 3, 4)
-CX_EXPORT int cxMapListIntersection(CxMap *dst, const CxMap *src, const CxList *keys,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4)
+int cxMapListIntersection(CxMap *dst, const CxMap *src, const CxList *keys,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 /**
@@ -595,8 +589,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull_arg(1, 2, 3)
-CX_EXPORT int cxMapUnion(CxMap *dst, const CxMap *src,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3)
+int cxMapUnion(CxMap *dst, const CxMap *src,
         cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data);
 
 /**
@@ -622,8 +616,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxMapClone()
  */
-cx_attr_nonnull
-CX_EXPORT int cxMapCloneShallow(CxMap *dst, const CxMap *src);
+CX_EXTERN CX_NONNULL
+int cxMapCloneShallow(CxMap *dst, const CxMap *src);
 
 /**
  * Clones entries of a map if their key is not present in another map.
@@ -637,8 +631,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull
-CX_EXPORT int cxMapDifferenceShallow(CxMap *dst, const CxMap *minuend, const CxMap *subtrahend);
+CX_EXTERN CX_NONNULL
+int cxMapDifferenceShallow(CxMap *dst, const CxMap *minuend, const CxMap *subtrahend);
 
 /**
  * Clones entries of a map if their key is not present in a list.
@@ -658,9 +652,8 @@
  * @retval non-zero when an allocation error occurred
  * @see cxMapListDifference()
  */
-cx_attr_nonnull
-CX_EXPORT int cxMapListDifferenceShallow(CxMap *dst, const CxMap *src, const CxList *keys);
-
+CX_EXTERN CX_NONNULL
+int cxMapListDifferenceShallow(CxMap *dst, const CxMap *src, const CxList *keys);
 
 /**
  * Clones entries of a map only if their key is present in another map.
@@ -674,8 +667,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull
-CX_EXPORT int cxMapIntersectionShallow(CxMap *dst, const CxMap *src, const CxMap *other);
+CX_EXTERN CX_NONNULL
+int cxMapIntersectionShallow(CxMap *dst, const CxMap *src, const CxMap *other);
 
 /**
  * Clones entries of a map only if their key is present in a list.
@@ -694,8 +687,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull
-CX_EXPORT int cxMapListIntersectionShallow(CxMap *dst, const CxMap *src, const CxList *keys);
+CX_EXTERN CX_NONNULL
+int cxMapListIntersectionShallow(CxMap *dst, const CxMap *src, const CxList *keys);
 
 /**
  * Clones entries into a map if their key does not exist yet.
@@ -713,9 +706,8 @@
  * @retval zero when the elements were successfully cloned
  * @retval non-zero when an allocation error occurred
  */
-cx_attr_nonnull
-CX_EXPORT int cxMapUnionShallow(CxMap *dst, const CxMap *src);
-
+CX_EXTERN CX_NONNULL
+int cxMapUnionShallow(CxMap *dst, const CxMap *src);
 
 /**
  * Compares the entries of two maps.
@@ -729,11 +721,7 @@
  * @retval non-zero (unspecified whether positive or negative) when the size
  * of both maps is equal but a key or a value is different
  */
-cx_attr_nonnull
-CX_EXPORT int cxMapCompare(const CxMap *map, const CxMap *other);
-
-#ifdef    __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NONNULL
+int cxMapCompare(const CxMap *map, const CxMap *other);
 
 #endif // UCX_MAP_H
--- a/ucx/cx/mempool.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/mempool.h	Wed Dec 31 16:40:12 2025 +0100
@@ -39,10 +39,6 @@
 #include "common.h"
 #include "allocator.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /** A memory block in a simple memory pool. */
 struct cx_mempool_memory_s {
     /** The destructor. */
@@ -156,7 +152,8 @@
  *
  * @param pool the memory pool to free
  */
-CX_EXPORT void cxMempoolFree(CxMempool *pool);
+CX_EXTERN
+void cxMempoolFree(CxMempool *pool);
 
 /**
  * Creates an array-based memory pool.
@@ -168,8 +165,8 @@
  * @param type the type of memory pool
  * @return the created memory pool or @c NULL if allocation failed
  */
-cx_attr_nodiscard cx_attr_malloc cx_attr_dealloc(cxMempoolFree, 1)
-CX_EXPORT CxMempool *cxMempoolCreate(size_t capacity, enum cx_mempool_type type);
+CX_EXTERN CX_NODISCARD CX_MALLOC CX_DEALLOC(cxMempoolFree, 1)
+CxMempool *cxMempoolCreate(size_t capacity, enum cx_mempool_type type);
 
 /**
  * Creates a basic array-based memory pool.
@@ -207,8 +204,8 @@
  * @param pool the memory pool
  * @param fnc the destructor that shall be applied to all memory blocks
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT void cxMempoolGlobalDestructor(CxMempool *pool, cx_destructor_func fnc);
+CX_EXTERN CX_NONNULL_ARG(1)
+void cxMempoolGlobalDestructor(CxMempool *pool, cx_destructor_func fnc);
 
 /**
  * Sets the global destructor for all memory blocks within the specified pool.
@@ -217,8 +214,8 @@
  * @param fnc the destructor that shall be applied to all memory blocks
  * @param data additional data for the destructor function
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT void cxMempoolGlobalDestructor2(CxMempool *pool, cx_destructor_func2 fnc, void *data);
+CX_EXTERN CX_NONNULL_ARG(1)
+void cxMempoolGlobalDestructor2(CxMempool *pool, cx_destructor_func2 fnc, void *data);
 
 /**
  * Sets the destructor function for a specific allocated memory object.
@@ -230,8 +227,8 @@
  * @param memory the object allocated in the pool
  * @param fnc the destructor function
  */
-cx_attr_nonnull
-CX_EXPORT void cxMempoolSetDestructor(void *memory, cx_destructor_func fnc);
+CX_EXTERN CX_NONNULL
+void cxMempoolSetDestructor(void *memory, cx_destructor_func fnc);
 
 /**
  * Sets the destructor function for a specific allocated memory object.
@@ -244,8 +241,8 @@
  * @param fnc the destructor function
  * @param data additional data for the destructor function
  */
-cx_attr_nonnull
-CX_EXPORT void cxMempoolSetDestructor2(void *memory, cx_destructor_func2 fnc, void *data);
+CX_EXTERN CX_NONNULL
+void cxMempoolSetDestructor2(void *memory, cx_destructor_func2 fnc, void *data);
 
 /**
  * Removes the destructor function for a specific allocated memory object.
@@ -255,8 +252,8 @@
  *
  * @param memory the object allocated in the pool
  */
-cx_attr_nonnull
-CX_EXPORT void cxMempoolRemoveDestructor(void *memory);
+CX_EXTERN CX_NONNULL
+void cxMempoolRemoveDestructor(void *memory);
 
 /**
  * Removes the destructor function for a specific allocated memory object.
@@ -266,8 +263,8 @@
  *
  * @param memory the object allocated in the pool
  */
-cx_attr_nonnull
-CX_EXPORT void cxMempoolRemoveDestructor2(void *memory);
+CX_EXTERN CX_NONNULL
+void cxMempoolRemoveDestructor2(void *memory);
 
 /**
  * Registers foreign memory with this pool.
@@ -284,9 +281,8 @@
  * @retval zero success
  * @retval non-zero failure
  */
-cx_attr_nonnull
-CX_EXPORT int cxMempoolRegister(CxMempool *pool, void *memory, cx_destructor_func destr);
-
+CX_EXTERN CX_NONNULL
+int cxMempoolRegister(CxMempool *pool, void *memory, cx_destructor_func destr);
 
 /**
  * Registers foreign memory with this pool.
@@ -307,8 +303,8 @@
  * @retval zero success
  * @retval non-zero failure
  */
-cx_attr_nonnull
-CX_EXPORT int cxMempoolRegister2(CxMempool *pool, void *memory, cx_destructor_func2 destr, void *data);
+CX_EXTERN CX_NONNULL
+int cxMempoolRegister2(CxMempool *pool, void *memory, cx_destructor_func2 destr, void *data);
 
 /**
  * Transfers all the memory managed by one pool to another.
@@ -325,8 +321,8 @@
  * @retval zero success
  * @retval non-zero allocation failure or incompatible pools
  */
-cx_attr_nonnull
-CX_EXPORT int cxMempoolTransfer(CxMempool *source, CxMempool *dest);
+CX_EXTERN CX_NONNULL
+int cxMempoolTransfer(CxMempool *source, CxMempool *dest);
 
 /**
  * Transfers an object from one pool to another.
@@ -342,11 +338,7 @@
  * @retval zero success
  * @retval non-zero failure, or the object was not found in the source pool, or the pools are incompatible
  */
-cx_attr_nonnull
-CX_EXPORT int cxMempoolTransferObject(CxMempool *source, CxMempool *dest, const void *obj);
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NONNULL
+int cxMempoolTransferObject(CxMempool *source, CxMempool *dest, const void *obj);
 
 #endif // UCX_MEMPOOL_H
--- a/ucx/cx/printf.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/printf.h	Wed Dec 31 16:40:12 2025 +0100
@@ -45,14 +45,9 @@
  * @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) \
+#define CX_PRINTF_ARGS(fmt_idx, arg_idx) \
     __attribute__((__format__(printf, fmt_idx, arg_idx)))
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
 /**
  * The maximum string length that fits into stack memory.
  */
@@ -68,8 +63,8 @@
  * @param ... additional arguments
  * @return the total number of bytes written or an error code from stdlib printf implementation
  */
-cx_attr_nonnull_arg(1, 2, 3) cx_attr_printf(3, 4) cx_attr_cstr_arg(3)
-CX_EXPORT int cx_fprintf(void *stream, cx_write_func wfc, const char *fmt, ...);
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3) CX_PRINTF_ARGS(3, 4) CX_CSTR_ARG(3)
+int cx_fprintf(void *stream, cx_write_func wfc, const char *fmt, ...);
 
 /**
  * A @c vfprintf like function which writes the output to a stream by
@@ -82,8 +77,8 @@
  * @return the total number of bytes written or an error code from stdlib printf implementation
  * @see cx_fprintf()
  */
-cx_attr_nonnull cx_attr_cstr_arg(3)
-CX_EXPORT int cx_vfprintf(void *stream, cx_write_func wfc, const char *fmt, va_list ap);
+CX_EXTERN CX_NONNULL CX_CSTR_ARG(3)
+int cx_vfprintf(void *stream, cx_write_func wfc, const char *fmt, va_list ap);
 
 /**
  * An @c asprintf like function which allocates space for a string
@@ -99,8 +94,8 @@
  * @return the formatted string
  * @see cx_strfree_a()
  */
-cx_attr_nonnull_arg(1, 2) cx_attr_printf(2, 3) cx_attr_cstr_arg(2)
-CX_EXPORT cxmutstr cx_asprintf_a(const CxAllocator *allocator, const char *fmt, ...);
+CX_EXTERN CX_NONNULL_ARG(1, 2) CX_PRINTF_ARGS(2, 3) CX_CSTR_ARG(2)
+cxmutstr cx_asprintf_a(const CxAllocator *allocator, const char *fmt, ...);
 
 /**
  * An @c asprintf like function which allocates space for a string
@@ -131,8 +126,8 @@
  * @return the formatted string
  * @see cx_asprintf_a()
  */
-cx_attr_nonnull cx_attr_cstr_arg(2)
-CX_EXPORT cxmutstr cx_vasprintf_a(const CxAllocator *allocator, const char *fmt, va_list ap);
+CX_EXTERN CX_NONNULL CX_CSTR_ARG(2)
+cxmutstr cx_vasprintf_a(const CxAllocator *allocator, const char *fmt, va_list ap);
 
 /**
 * A @c vasprintf like function which allocates space for a string
@@ -193,8 +188,8 @@
  * @param ... additional arguments
  * @return the length of the produced string or an error code from stdlib printf implementation
  */
-cx_attr_nonnull_arg(1, 2, 3, 4) cx_attr_printf(4, 5) cx_attr_cstr_arg(4)
-CX_EXPORT int cx_sprintf_a(const CxAllocator *alloc, char **str, size_t *len, const char *fmt, ...);
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4) CX_PRINTF_ARGS(4, 5) CX_CSTR_ARG(4)
+int cx_sprintf_a(const CxAllocator *alloc, char **str, size_t *len, const char *fmt, ...);
 
 
 /**
@@ -228,8 +223,8 @@
  * @param ap argument list
  * @return the length of the produced string or an error code from stdlib printf implementation
  */
-cx_attr_nonnull  cx_attr_cstr_arg(4) cx_attr_access_rw(2) cx_attr_access_rw(3)
-CX_EXPORT int cx_vsprintf_a(const CxAllocator *alloc, char **str, size_t *len, const char *fmt, va_list ap);
+CX_EXTERN CX_NONNULL  CX_CSTR_ARG(4) CX_ACCESS_RW(2) CX_ACCESS_RW(3)
+int cx_vsprintf_a(const CxAllocator *alloc, char **str, size_t *len, const char *fmt, va_list ap);
 
 
 /**
@@ -275,9 +270,9 @@
  * @param ... additional arguments
  * @return the length of the produced string or an error code from stdlib printf implementation
  */
-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)
-CX_EXPORT int cx_sprintf_sa(const CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, ...);
+CX_EXTERN CX_NONNULL_ARG(1, 2, 4, 5) CX_PRINTF_ARGS(5, 6) CX_CSTR_ARG(5)
+CX_ACCESS_RW(2) CX_ACCESS_RW(3) CX_ACCESS_RW(4)
+int cx_sprintf_sa(const 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.
@@ -322,12 +317,7 @@
  * @param ap argument list
  * @return the length of the produced string or an error code from stdlib printf implementation
  */
-cx_attr_nonnull cx_attr_cstr_arg(5)
-CX_EXPORT int cx_vsprintf_sa(const CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, va_list ap);
-
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NONNULL CX_CSTR_ARG(5)
+int cx_vsprintf_sa(const CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, va_list ap);
 
 #endif //UCX_PRINTF_H
--- a/ucx/cx/properties.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/properties.h	Wed Dec 31 16:40:12 2025 +0100
@@ -41,10 +41,6 @@
 #include "map.h"
 #include "buffer.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 /**
  * Configures the expected characters for the properties parser.
  */
@@ -184,8 +180,8 @@
  * @param config the properties configuration
  * @see cxPropertiesInitDefault()
  */
-cx_attr_nonnull
-CX_EXPORT void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
+CX_EXTERN CX_NONNULL
+void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
 
 /**
  * Destroys the properties interface.
@@ -198,8 +194,8 @@
  *
  * @param prop the properties interface
  */
-cx_attr_nonnull
-CX_EXPORT void cxPropertiesDestroy(CxProperties *prop);
+CX_EXTERN CX_NONNULL
+void cxPropertiesDestroy(CxProperties *prop);
 
 /**
  * Destroys and re-initializes the properties interface.
@@ -209,8 +205,8 @@
  *
  * @param prop the properties interface
  */
-cx_attr_nonnull
-CX_EXPORT void cxPropertiesReset(CxProperties *prop);
+CX_EXTERN CX_NONNULL
+void cxPropertiesReset(CxProperties *prop);
 
 /**
  * Initialize a properties parser with the default configuration.
@@ -242,8 +238,8 @@
  * @retval non-zero a memory allocation was necessary but failed
  * @see cxPropertiesFill()
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
-CX_EXPORT int cxPropertiesFilln(CxProperties *prop, const char *buf, size_t len);
+CX_EXTERN CX_NONNULL CX_ACCESS_R(2, 3)
+int cxPropertiesFilln(CxProperties *prop, const char *buf, size_t len);
 
 /**
  * Internal function, do not use.
@@ -253,8 +249,8 @@
  * @retval zero success
  * @retval non-zero a memory allocation was necessary but failed
  */
-cx_attr_nonnull
-CX_INLINE int cx_properties_fill(CxProperties *prop, cxstring str) {
+CX_NONNULL CX_INLINE
+int cx_properties_fill(CxProperties *prop, cxstring str) {
     return cxPropertiesFilln(prop, str.ptr, str.length);
 }
 
@@ -287,8 +283,8 @@
  * @param buf a pointer to stack memory
  * @param capacity the capacity of the stack memory
  */
-cx_attr_nonnull
-CX_EXPORT void cxPropertiesUseStack(CxProperties *prop, char *buf, size_t capacity);
+CX_EXTERN CX_NONNULL
+void cxPropertiesUseStack(CxProperties *prop, char *buf, size_t capacity);
 
 /**
  * Retrieves the next key/value-pair.
@@ -320,8 +316,8 @@
  * @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
-CX_EXPORT CxPropertiesStatus cxPropertiesNext(CxProperties *prop, cxstring *key, cxstring *value);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxPropertiesStatus cxPropertiesNext(CxProperties *prop, cxstring *key, cxstring *value);
 
 /**
  * The size of the stack memory that `cxPropertiesLoad()` will reserve with `cxPropertiesUseStack()`.
@@ -342,8 +338,8 @@
  * @param config the parser config
  * @return status code
  */
-cx_attr_nonnull_arg(3)
-CX_EXPORT CxPropertiesStatus cx_properties_load(const CxAllocator *allocator,
+CX_EXTERN CX_NONNULL_ARG(3)
+CxPropertiesStatus cx_properties_load(const CxAllocator *allocator,
         cxstring filename, CxMap *target, CxPropertiesConfig config);
 
 /**
@@ -401,9 +397,4 @@
 #define cxPropertiesLoadDefault(allocator, filename, target) \
     cx_properties_load(allocator, cx_strcast(filename), target, cx_properties_config_default)
 
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
-
 #endif // UCX_PROPERTIES_H
--- a/ucx/cx/streams.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/streams.h	Wed Dec 31 16:40:12 2025 +0100
@@ -36,15 +36,11 @@
  * @copyright 2-Clause BSD License
  */
 
-#ifndef Ucx_strEAMS_H
-#define Ucx_strEAMS_H
+#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.
  *
@@ -61,9 +57,9 @@
  * 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)
-CX_EXPORT size_t cx_stream_bncopy(void *src, void *dest,
+CX_EXTERN CX_NONNULL_ARG(1, 2, 3, 4)
+CX_ACCESS_R(1) CX_ACCESS_W(2) CX_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);
 
@@ -95,8 +91,8 @@
  * @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)
-CX_EXPORT size_t cx_stream_ncopy(void *src, void *dest,
+CX_EXTERN CX_NONNULL CX_ACCESS_R(1) CX_ACCESS_W(2)
+size_t cx_stream_ncopy(void *src, void *dest,
         cx_read_func rfnc, cx_write_func wfnc, size_t n);
 
 /**
@@ -113,8 +109,4 @@
 #define cx_stream_copy(src, dest, rfnc, wfnc) \
     cx_stream_ncopy(src, dest, rfnc, wfnc, SIZE_MAX)
 
-#ifdef __cplusplus
-}
-#endif
-
-#endif // Ucx_strEAMS_H
+#endif // UCX_STREAMS_H
--- a/ucx/cx/string.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/string.h	Wed Dec 31 16:40:12 2025 +0100
@@ -33,14 +33,17 @@
  * @copyright 2-Clause BSD License
  */
 
-#ifndef Ucx_strING_H
-#define Ucx_strING_H
+#ifndef UCX_STRING_H
+#define UCX_STRING_H
 
 #include "common.h"
 #include "allocator.h"
 
 #include <string.h>
 
+/** Convenience macro for creating a null string */
+#define CX_NULLSTR cx_mutstr(NULL)
+
 /** Expands a UCX string as printf arguments. */
 #define CX_SFMT(s) (int) (s).length, (s).ptr
 
@@ -156,7 +159,7 @@
  *
  * @see cx_mutstrn()
  */
-cx_attr_nodiscard cx_attr_cstr_arg(1)
+CX_NODISCARD CX_CSTR_ARG(1)
 CX_INLINE cxmutstr cx_mutstr(char *cstring) {
     cxmutstr str;
     str.ptr = cstring;
@@ -180,7 +183,7 @@
  *
  * @see cx_mutstr()
  */
-cx_attr_nodiscard cx_attr_access_rw(1, 2)
+CX_NODISCARD CX_ACCESS_RW(1, 2)
 CX_INLINE cxmutstr cx_mutstrn(char *cstring, size_t length) {
     cxmutstr str;
     str.ptr = cstring;
@@ -205,7 +208,7 @@
  *
  * @see cx_strn()
  */
-cx_attr_nodiscard cx_attr_cstr_arg(1)
+CX_NODISCARD CX_CSTR_ARG(1)
 CX_INLINE cxstring cx_str(const char *cstring) {
     cxstring str;
     str.ptr = cstring;
@@ -230,7 +233,7 @@
  *
  * @see cx_str()
  */
-cx_attr_nodiscard cx_attr_access_r(1, 2)
+CX_NODISCARD CX_ACCESS_R(1, 2)
 CX_INLINE cxstring cx_strn(const char *cstring, size_t length) {
     cxstring str;
     str.ptr = cstring;
@@ -239,42 +242,60 @@
 }
 
 #ifdef __cplusplus
-cx_attr_nodiscard
-CX_CPPDECL cxstring cx_strcast(cxmutstr str) {
-    return cx_strn(str.ptr, str.length);
+CX_NODISCARD
+CX_CPPDECL cxmutstr cx_strcast_m(cxmutstr str) {
+    return str;
 }
-cx_attr_nodiscard
-CX_CPPDECL cxstring cx_strcast(cxstring str) {
+CX_NODISCARD
+CX_CPPDECL cxstring cx_strcast_m(cxstring str) {
     return str;
 }
-cx_attr_nodiscard
-CX_CPPDECL cxstring cx_strcast(const char *str) {
+CX_NODISCARD
+CX_CPPDECL cxmutstr cx_strcast_m(char *str) {
+    return cx_mutstr(str);
+}
+CX_NODISCARD
+CX_CPPDECL cxmutstr cx_strcast_m(unsigned char *str) {
+    return cx_mutstr(reinterpret_cast<char*>(str));
+}
+CX_NODISCARD
+CX_CPPDECL cxstring cx_strcast_m(const char *str) {
     return cx_str(str);
 }
-cx_attr_nodiscard
-CX_CPPDECL cxstring cx_strcast(const unsigned char *str) {
+CX_NODISCARD
+CX_CPPDECL cxstring cx_strcast_m(const unsigned char *str) {
     return cx_str(reinterpret_cast<const char*>(str));
 }
-extern "C" {
+CX_NODISCARD
+CX_CPPDECL cxstring cx_strcast_(cxmutstr str) {
+    return cx_strn(str.ptr, str.length);
+}
+CX_NODISCARD
+CX_CPPDECL cxstring cx_strcast_(cxstring str) {
+    return str;
+}
+#define cx_strcast(s) cx_strcast_(cx_strcast_m(s))
 #else
 /**
  * Internal function, do not use.
  * @param str
  * @return
+ * @see cx_strcast_m()
  * @see cx_strcast()
  */
-cx_attr_nodiscard
-CX_INLINE cxstring cx_strcast_m(cxmutstr str) {
-    return (cxstring) {str.ptr, str.length};
+CX_NODISCARD
+CX_INLINE cxmutstr cx_strcast_cxms(cxmutstr str) {
+    return str;
 }
 /**
  * Internal function, do not use.
  * @param str
  * @return
+ * @see cx_strcast_m()
  * @see cx_strcast()
  */
-cx_attr_nodiscard
-CX_INLINE cxstring cx_strcast_c(cxstring str) {
+CX_NODISCARD
+CX_INLINE cxstring cx_strcast_cxs(cxstring str) {
     return str;
 }
 
@@ -282,10 +303,35 @@
  * Internal function, do not use.
  * @param str
  * @return
+ * @see cx_strcast_m()
+ * @see cx_strcast()
+ */
+CX_NODISCARD
+CX_INLINE cxmutstr cx_strcast_uc(unsigned char *str) {
+    return cx_mutstr((char*)str);
+}
+
+/**
+ * Internal function, do not use.
+ * @param str
+ * @return
+ * @see cx_strcast_m()
  * @see cx_strcast()
  */
-cx_attr_nodiscard
-CX_INLINE cxstring cx_strcast_u(const unsigned char *str) {
+CX_NODISCARD
+CX_INLINE cxmutstr cx_strcast_c(char *str) {
+    return cx_mutstr(str);
+}
+
+/**
+ * Internal function, do not use.
+ * @param str
+ * @return
+ * @see cx_strcast_m()
+ * @see cx_strcast()
+ */
+CX_NODISCARD
+CX_INLINE cxstring cx_strcast_ucc(const unsigned char *str) {
     return cx_str((const char*)str);
 }
 
@@ -293,10 +339,11 @@
  * Internal function, do not use.
  * @param str
  * @return
+ * @see cx_strcast_m()
  * @see cx_strcast()
  */
-cx_attr_nodiscard
-CX_INLINE cxstring cx_strcast_z(const char *str) {
+CX_NODISCARD
+CX_INLINE cxstring cx_strcast_cc(const char *str) {
     return cx_str(str);
 }
 
@@ -304,18 +351,62 @@
  * Wraps any string into an UCX string.
  *
  * @param str (any supported string type) the string to cast
- * @return (@c cxstring) the string wrapped as UCX string
+ * @return (@c cxstring) or (@c cxmutstr) the string wrapped as UCX string
+ */
+#define cx_strcast_m(str) _Generic((str), \
+        cxstring: cx_strcast_cxs, \
+        cxmutstr: cx_strcast_cxms, \
+        const unsigned char*: cx_strcast_ucc, \
+        unsigned char *: cx_strcast_uc, \
+        const char*: cx_strcast_cc, \
+        char *: cx_strcast_c) (str)
+
+/**
+ * Internal function, do not use.
+ * @param str
+ * @return
+ */
+CX_INLINE cxstring cx_strcast_1(cxmutstr str) {
+    return (cxstring){str.ptr, str.length};
+}
+
+/**
+ * Internal function, do not use.
+ * @param str
+ * @return
  */
-#define cx_strcast(str) _Generic((str), \
-        cxmutstr: cx_strcast_m, \
-        cxstring: cx_strcast_c, \
-        const unsigned char*: cx_strcast_u, \
-        unsigned char *: cx_strcast_u, \
-        const char*: cx_strcast_z, \
-        char *: cx_strcast_z) (str)
+CX_INLINE cxstring cx_strcast_2(cxstring str) {
+    return str;
+}
+
+/** internal conversion macro */
+#define cx_strcast_(str) _Generic((str), \
+        cxmutstr: cx_strcast_1, \
+        cxstring: cx_strcast_2)(str)
+
+/**
+ * Converts any string to a cxstring.
+ *
+ * @param str (any supported string type) the string to cast
+ * @return he string converted to a (@c cxstring)
+ */
+#define cx_strcast(str) cx_strcast_(cx_strcast_m(str))
 #endif
 
 /**
+ * Casts away constness and converts a cxstring to a cxmutstr.
+ * For internal use only!
+ * @param str
+ * @return
+ */
+CX_INLINE cxmutstr cx_mutstrcast(cxstring str) {
+    cxmutstr s;
+    s.ptr = (char*)str.ptr;
+    s.length = str.length;
+    return s;
+}
+
+/**
  * Passes the pointer in this string to the cxDefaultAllocator's @c free() function.
  *
  * The pointer in the struct is set to @c NULL, and the length is set to zero,
@@ -327,7 +418,8 @@
  *
  * @param str the string to free
  */
-CX_EXPORT void cx_strfree(cxmutstr *str);
+CX_EXTERN
+void cx_strfree(cxmutstr *str);
 
 /**
  * Passes the pointer in this string to the allocator's free function.
@@ -342,8 +434,24 @@
  * @param alloc the allocator
  * @param str the string to free
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT void cx_strfree_a(const CxAllocator *alloc, cxmutstr *str);
+CX_EXTERN CX_NONNULL_ARG(1)
+void cx_strfree_a(const CxAllocator *alloc, cxmutstr *str);
+
+/**
+ * Copies a string.
+ *
+ * Internal function - do not use.
+ *
+ * @param alloc the allocator
+ * @param dest a pointer to the structure where to copy the contents to
+ * @param src the source string
+ *
+ * @retval zero success
+ * @retval non-zero if re-allocation failed
+ * @see cx_strcpy_a()
+ */
+CX_EXTERN CX_NONNULL_ARG(1)
+int cx_strcpy_a_(const CxAllocator *alloc, cxmutstr *dest, cxstring src);
 
 /**
  * Copies a string.
@@ -353,16 +461,13 @@
  *
  * The string in @p dest is guaranteed to be zero-terminated, regardless of whether @p src is.
  *
- * @param alloc the allocator
- * @param dest a pointer to the structure where to copy the contents to
+ * @param alloc (@c CxAllocator*) the allocator
+ * @param dest (@c cxmutstr*) a pointer to the structure where to copy the contents to
  * @param src the source string
- *
  * @retval zero success
  * @retval non-zero if re-allocation failed
  */
-cx_attr_nonnull_arg(1)
-CX_EXPORT int cx_strcpy_a(const CxAllocator *alloc, cxmutstr *dest, cxstring src);
-
+#define cx_strcpy_a(alloc, dest, src) cx_strcpy_a_(alloc, dest, cx_strcast(src))
 
 /**
  * Copies a string.
@@ -373,8 +478,7 @@
  * The string in @p dest is guaranteed to be zero-terminated, regardless of whether @p src is.
  *
  * @param dest (@c cxmutstr*) a pointer to the structure where to copy the contents to
- * @param src (@c cxstring) the source string
- *
+ * @param src the source string
  * @retval zero success
  * @retval non-zero if re-allocation failed
  */
@@ -392,8 +496,8 @@
  * @param ...      all strings
  * @return the accumulated length of all strings
  */
-cx_attr_nodiscard
-CX_EXPORT size_t cx_strlen(size_t count, ...);
+CX_EXTERN CX_NODISCARD
+size_t cx_strlen(size_t count, ...);
 
 /**
  * Concatenates strings.
@@ -404,12 +508,10 @@
  * If @p str already contains a string, the memory will be reallocated and
  * the other strings are appended. Otherwise, new memory is allocated.
  *
- * 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.
+ * If allocation fails, the @c ptr in the returned string will be @c NULL.
  *
  * @param alloc the allocator to use
  * @param str   the string the other strings shall be concatenated to
@@ -417,92 +519,111 @@
  * @param ...   all other UCX strings
  * @return the concatenated string
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT cxmutstr cx_strcat_ma(const CxAllocator *alloc,
+CX_EXTERN CX_NONNULL
+cxmutstr cx_strcat_a(const CxAllocator *alloc,
         cxmutstr str, size_t count, ...);
 
 /**
  * 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.
- *
-* 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 (@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__)
-
-/**
- * Concatenates strings and returns a new string.
- *
  * The resulting string will be allocated by the cxDefaultAllocator.
  * So developers @em must pass the return value to cx_strfree() eventually.
  *
-* 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.
+ * If allocation fails, the @c ptr in the returned string will be @c NULL.
  *
+ * @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 UCX strings
- * @return (@c cxmutstr) the concatenated string
+ * @return the concatenated string
+ */
+#define cx_strcat(str, count, ...) \
+        cx_strcat_a(cxDefaultAllocator, str, count, __VA_ARGS__)
+
+/**
+ * Returns a substring.
+ *
+ * Internal function - do not use.
+ *
+ * @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
+ * @see cx_strsubsl()
  */
-#define cx_strcat(count, ...) \
-        cx_strcat_ma(cxDefaultAllocator, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
+CX_EXTERN CX_NODISCARD
+cxstring cx_strsubsl_(cxstring string, size_t start, size_t length);
+
+/**
+ * Returns a substring.
+ *
+ * Internal function - do not use.
+ *
+ * @param string input string
+ * @param start  start location of the substring
+ * @return a substring of @p string starting at @p start
+ * @see cx_strsubs()
+ */
+CX_EXTERN CX_NODISCARD
+cxstring cx_strsubs_(cxstring string, size_t start);
 
 /**
- * Concatenates strings.
- *
- * The resulting string will be allocated by the cxDefaultAllocator.
- * 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
- * the other strings are appended. Otherwise, new memory is allocated.
- *
-* 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 (@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
+ * Internal conversion function - do not use.
+ * @param string
+ * @param start
+ * @return
+ */
+CX_INLINE
+cxmutstr cx_strsubs_m_(cxmutstr string, size_t start) {
+    return cx_mutstrcast(cx_strsubs_(cx_strcast(string), start));
+}
+
+/**
+ * Internal conversion function - do not use.
+ * @param string
+ * @param start
+ * @param length
+ * @return
  */
-#define cx_strcat_m(str, count, ...) \
-        cx_strcat_ma(cxDefaultAllocator, str, count, __VA_ARGS__)
+CX_INLINE
+cxmutstr cx_strsubsl_m_(cxmutstr string, size_t start, size_t length) {
+    return cx_mutstrcast(cx_strsubsl_(cx_strcast(string), start, length));
+}
 
+#ifdef __cplusplus
+CX_CPPDECL cxstring cx_strsubs_cpp_(cxstring string, size_t start) {
+    return cx_strsubs_(string, start);
+}
+CX_CPPDECL cxstring cx_strsubsl_cpp_(cxstring string, size_t start, size_t length) {
+    return cx_strsubsl_(string, start, length);
+}
+CX_CPPDECL cxmutstr cx_strsubs_cpp_(cxmutstr string, size_t start) {
+    return cx_strsubs_m_(string, start);
+}
+CX_CPPDECL cxmutstr cx_strsubsl_cpp_(cxmutstr string, size_t start, size_t length) {
+    return cx_strsubsl_m_(string, start, length);
+}
+#define cx_strsubs(string, start) cx_strsubs_cpp_(cx_strcast_m(string), start)
+#define cx_strsubsl(string, start, length) cx_strsubsl_cpp_(cx_strcast_m(string), start, length)
+#else
 /**
  * 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.
+ * input string and is @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
+ * @param start (@c size_t) start location of the substring
+ * @return (@c cxstring or @c cxmutstr) a substring of @p string starting at @p start
  *
  * @see cx_strsubsl()
- * @see cx_strsubs_m()
- * @see cx_strsubsl_m()
  */
-cx_attr_nodiscard
-CX_EXPORT cxstring cx_strsubs(cxstring string, size_t start);
+#define cx_strsubs(string, start) _Generic(cx_strcast_m(string), \
+        cxstring: cx_strsubs_, \
+        cxmutstr: cx_strsubs_m_)(cx_strcast_m(string), start)
 
 /**
  * Returns a substring starting at the specified location.
@@ -520,51 +641,107 @@
  * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubs()
- * @see cx_strsubs_m()
- * @see cx_strsubsl_m()
+ */
+#define cx_strsubsl(string, start, length) _Generic(cx_strcast_m(string), \
+        cxstring: cx_strsubsl_, \
+        cxmutstr: cx_strsubsl_m_)(cx_strcast_m(string), start, length)
+#endif
+
+/**
+ * Returns the character at the specified index offset.
+ *
+ * Internal function - do not use.
+ *
+ * @param str the string
+ * @param index the index offset
+ * @return the character at the index
+ * @see cx_strat()
  */
-cx_attr_nodiscard
-CX_EXPORT cxstring cx_strsubsl(cxstring string, size_t start, size_t length);
+CX_INLINE
+char cx_strat_(cxstring str, off_t index) {
+    size_t i;
+    if (index >= 0) {
+        i = index;
+    } else {
+        i = (size_t) (str.length + index);
+    }
+    if (i >= str.length) {
+        return '\0';
+    }
+    return str.ptr[i];
+}
+
+/**
+ * Returns the character at the specified index offset.
+ *
+ * When the @p index is negative, the character is counted from the end of the
+ * string where -1 denotes the last character in the string.
+ *
+ * When the @p index is out of bounds, the function returns zero.
+ *
+ * @param str the string
+ * @param index the index offset
+ * @return the character at the index
+ * @see cx_strat()
+ */
+#define cx_strat(str, index) cx_strat_(cx_strcast(str), index)
 
 /**
- * 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.
- * 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
- *
- * @see cx_strsubsl_m()
- * @see cx_strsubs()
- * @see cx_strsubsl()
+ * Searches for a character in a string.
+ * Internal function - do not use.
+ * @param string
+ * @param chr
+ * @return
+ * @see cx_strchr()
  */
-cx_attr_nodiscard
-CX_EXPORT cxmutstr cx_strsubs_m(cxmutstr string, size_t start);
+CX_EXTERN CX_NODISCARD
+cxstring cx_strchr_(cxstring string, int chr);
 
 /**
- * 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.
- *
- * @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
- *
- * @see cx_strsubs_m()
- * @see cx_strsubs()
- * @see cx_strsubsl()
+ * Searches for a character in a string.
+ * Internal function - do not use.
+ * @param string
+ * @param chr
+ * @return
+ * @see cx_strrchr()
  */
-cx_attr_nodiscard
-CX_EXPORT cxmutstr cx_strsubsl_m(cxmutstr string, size_t start, size_t length);
+CX_EXTERN CX_NODISCARD
+cxstring cx_strrchr_(cxstring string, int chr);
+
+#ifdef __cplusplus
+CX_CPPDECL cxstring cx_strchr_cpp_(cxstring string, int chr) {
+    return cx_strchr_(string, chr);
+}
+CX_CPPDECL cxmutstr cx_strchr_cpp_(cxmutstr string, int chr) {
+    return cx_mutstrcast(cx_strchr_(cx_strcast(string), chr));
+}
+#define cx_strchr(s, chr) cx_strchr_cpp_(cx_strcast_m(s), chr)
+CX_CPPDECL cxstring cx_strrchr_cpp_(cxstring string, int chr) {
+    return cx_strrchr_(string, chr);
+}
+CX_CPPDECL cxmutstr cx_strrchr_cpp_(cxmutstr string, int chr) {
+    return cx_mutstrcast(cx_strrchr_(cx_strcast(string), chr));
+}
+#define cx_strrchr(s, chr) cx_strrchr_cpp_(cx_strcast_m(s), chr)
+#else
+/**
+ * Internal conversion function - do not use.
+ * @param string
+ * @param chr
+ * @return
+ */
+CX_INLINE cxmutstr cx_strchr_m_(cxmutstr string, int chr) {
+    return cx_mutstrcast(cx_strchr_(cx_strcast(string), chr));
+}
+/**
+ * Internal conversion function - do not use.
+ * @param string
+ * @param chr
+ * @return
+ */
+CX_INLINE  cxmutstr cx_strrchr_m_(cxmutstr string, int chr) {
+    return cx_mutstrcast(cx_strrchr_(cx_strcast(string), chr));
+}
 
 /**
  * Returns a substring starting at the location of the first occurrence of the
@@ -573,28 +750,13 @@
  * If the string does not contain the character, an empty string is returned.
  *
  * @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
- *
- * @see cx_strchr_m()
+ * @param chr (@c int) the character to locate
+ * @return (@c cxstring or @c cxmutstr) a substring starting at the first
+ * location of @p chr
  */
-cx_attr_nodiscard
-CX_EXPORT cxstring cx_strchr(cxstring string, int chr);
-
-/**
- * Returns a substring starting at the location of the first occurrence of the
- * specified character.
- *
- * If the string does not contain the character, an empty string is returned.
- *
- * @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
- *
- * @see cx_strchr()
- */
-cx_attr_nodiscard
-CX_EXPORT cxmutstr cx_strchr_m(cxmutstr string, int chr);
+#define cx_strchr(string, chr) _Generic(cx_strcast_m(string), \
+        cxstring: cx_strchr_, \
+        cxmutstr: cx_strchr_m_)(cx_strcast_m(string), chr)
 
 /**
  * Returns a substring starting at the location of the last occurrence of the
@@ -603,28 +765,47 @@
  * If the string does not contain the character, an empty string is returned.
  *
  * @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
- *
- * @see cx_strrchr_m()
+ * @param chr (@c int) the character to locate
+ * @return (@c cxstring or @c cxmutstr) a substring starting at the last
+ * location of @p chr
  */
-cx_attr_nodiscard
-CX_EXPORT cxstring cx_strrchr(cxstring string, int chr);
+#define cx_strrchr(string, chr) _Generic(cx_strcast_m(string), \
+        cxstring: cx_strrchr_, \
+        cxmutstr: cx_strrchr_m_)(cx_strcast_m(string), chr)
+#endif
 
 /**
- * Returns a substring starting at the location of the last occurrence of the
- * specified character.
+ * Searches for a specific substring.
  *
- * If the string does not contain the character, an empty string is returned.
+ * Internal function - do not use.
  *
- * @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
- *
- * @see cx_strrchr()
+ * @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 contained
+ * @see cx_strstr()
  */
-cx_attr_nodiscard
-CX_EXPORT cxmutstr cx_strrchr_m(cxmutstr string, int chr);
+CX_EXTERN CX_NODISCARD
+cxstring cx_strstr_(cxstring haystack, cxstring needle);
+
+#ifdef __cplusplus
+CX_CPPDECL cxstring cx_strstr_cpp_(cxstring haystack, cxstring needle) {
+    return cx_strstr_(haystack, needle);
+}
+CX_CPPDECL cxmutstr cx_strstr_cpp_(cxmutstr haystack, cxstring needle) {
+    return cx_mutstrcast(cx_strstr_(cx_strcast(haystack), needle));
+}
+#define cx_strstr(h,n) cx_strstr_cpp_(cx_strcast_m(h), cx_strcast(n))
+#else
+/**
+ * Internal conversion - do not use.
+ * @param haystack
+ * @param needle
+ * @return
+ */
+CX_INLINE  cxmutstr cx_strstr_m_(cxmutstr haystack, cxstring needle) {
+    return cx_mutstrcast(cx_strstr_(cx_strcast(haystack), needle));
+}
 
 /**
  * Returns a substring starting at the location of the first occurrence of the
@@ -636,34 +817,106 @@
  * 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
- *               contained
- * @see cx_strstr_m()
+ * @param needle string containing the sequence of characters to match
+ * @return (@c cxstring or @c cxmutstr) a substring starting at the first
+ * occurrence of @p needle, or an empty string, if the sequence is not contained
+ */
+#define cx_strstr(haystack, needle) _Generic(cx_strcast_m(haystack), \
+        cxstring: cx_strstr_,\
+        cxmutstr: cx_strstr_m_)(cx_strcast_m(haystack), cx_strcast(needle))
+#endif
+
+/**
+ * Splits a given string using a delimiter string.
+ *
+ * Internal function - do not use.
+ *
+ * @param string the string to split
+ * @param delim  the delimiter
+ * @param limit the maximum number of split items
+ * @param output the output array
+ * @return the actual number of split items
+ * @see cx_strsplit()
  */
-cx_attr_nodiscard
-CX_EXPORT cxstring cx_strstr(cxstring haystack, cxstring needle);
+CX_EXTERN CX_NODISCARD CX_NONNULL CX_ACCESS_W(4, 3)
+size_t cx_strsplit_(cxstring string, cxstring delim,
+        size_t limit, cxstring *output);
+
+/**
+ * Splits a given string using a delimiter string.
+ *
+ * Internal function - do not use.
+ *
+ * @param allocator the allocator to use for allocating the resulting array
+ * @param string the string to split
+ * @param delim  the delimiter
+ * @param limit the maximum number of split items
+ * @param output the output array
+ * @return the actual number of split items
+ * @see cx_strsplit_a()
+ */
+CX_EXTERN CX_NODISCARD CX_NONNULL CX_ACCESS_W(5)
+size_t cx_strsplit_a_(const CxAllocator *allocator,
+        cxstring string, cxstring delim,
+        size_t limit, cxstring **output);
+
 
 /**
- * Returns a substring starting at the location of the first occurrence of the
- * specified string.
+ * Splits a given string using a delimiter string.
+ *
+ * Internal function - do not use.
  *
- * If @p haystack does not contain @p needle, an empty string is returned.
+ * @param string the string to split
+ * @param delim  the delimiter
+ * @param limit the maximum number of split items
+ * @param output the output array
+ * @return the actual number of split items
+ * @see cx_strsplit_m()
+ */
+CX_EXTERN CX_NODISCARD CX_NONNULL CX_ACCESS_W(4, 3)
+size_t cx_strsplit_m_(cxmutstr string, cxstring delim,
+        size_t limit, cxmutstr *output);
+
+/**
+ * Splits a given string using a delimiter string.
  *
- * If @p needle is an empty string, the complete @p haystack is
- * returned.
+ * Internal function - do not use.
  *
- * @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
- *               contained
- * @see cx_strstr()
+ * @param allocator the allocator to use for allocating the resulting array
+ * @param string the string to split
+ * @param delim  the delimiter
+ * @param limit the maximum number of split items
+ * @param output the output array
+ * @return the actual number of split items
+ * @see cx_strsplit_ma()
  */
-cx_attr_nodiscard
-CX_EXPORT cxmutstr cx_strstr_m(cxmutstr haystack, cxstring needle);
+CX_EXTERN CX_NODISCARD CX_NONNULL CX_ACCESS_W(5)
+size_t cx_strsplit_ma_(const CxAllocator *allocator,
+        cxmutstr string, cxstring delim, size_t limit,
+        cxmutstr **output);
 
+#ifdef __cplusplus
+CX_CPPDECL size_t cx_strsplit_cpp_(cxstring string, cxstring delim,
+        size_t limit, cxstring *output) {
+    return cx_strsplit_(string, delim, limit, output);
+}
+CX_CPPDECL size_t cx_strsplit_cpp_(cxmutstr string, cxstring delim,
+        size_t limit, cxmutstr *output) {
+    return cx_strsplit_m_(string, delim, limit, output);
+}
+CX_CPPDECL size_t cx_strsplit_a_cpp_(const CxAllocator *allocator,
+        cxstring string, cxstring delim, size_t limit, cxstring **output) {
+    return cx_strsplit_a_(allocator, string, delim, limit, output);
+}
+CX_CPPDECL size_t cx_strsplit_a_cpp_(const CxAllocator *allocator,
+        cxmutstr string, cxstring delim, size_t limit, cxmutstr **output) {
+    return cx_strsplit_ma_(allocator, string, delim, limit, output);
+}
+#define cx_strsplit(string, delim, limit, output) \
+        cx_strsplit_cpp_(cx_strcast_m(string), cx_strcast(delim), limit, output)
+#define cx_strsplit_a(allocator, string, delim, limit, output) \
+        cx_strsplit_a_cpp_(allocator, cx_strcast_m(string), cx_strcast(delim), limit, output)
+#else
 /**
  * Splits a given string using a delimiter string.
  *
@@ -671,14 +924,17 @@
  * @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 preallocated array of at least @p limit length
+ * @param delim the delimiter
+ * @param limit (@c size_t) the maximum number of split items
+ * @param output (@c cxstring* or @c cxmutstr*) a preallocated array of at
+ * least @p limit length
  * @return the actual number of split items
  */
-cx_attr_nodiscard cx_attr_nonnull cx_attr_access_w(4, 3)
-CX_EXPORT size_t cx_strsplit(cxstring string, cxstring delim,
-        size_t limit, cxstring *output);
+#define cx_strsplit(string, delim, limit, output) \
+        _Generic(cx_strcast_m(string), \
+        cxstring: cx_strsplit_, \
+        cxmutstr: cx_strsplit_m_)\
+        (cx_strcast_m(string), cx_strcast(delim), limit, output)
 
 /**
  * Splits a given string using a delimiter string.
@@ -691,59 +947,20 @@
  * @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 allocator (@c CxAllocator*) the allocator to use for allocating the resulting array
  * @param string the string to split
  * @param delim  the delimiter
- * @param limit the maximum number of split items
- * @param output a pointer where the address of the allocated array shall be
- * written to
- * @return the actual number of split items
- */
-cx_attr_nodiscard cx_attr_nonnull cx_attr_access_w(5)
-CX_EXPORT size_t cx_strsplit_a(const CxAllocator *allocator,
-        cxstring string, cxstring delim,
-        size_t limit, cxstring **output);
-
-
-/**
- * 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.
- *
- * @param string the string to split
- * @param delim  the delimiter
- * @param limit the maximum number of split items
- * @param output a preallocated array of at least @p limit length
+ * @param limit (@c size_t) the maximum number of split items
+ * @param output (@c cxstring** or @c cxmutstr**) a pointer where the address
+ * of the allocated array shall be written to
  * @return the actual number of split items
  */
-cx_attr_nodiscard cx_attr_nonnull cx_attr_access_w(4, 3)
-CX_EXPORT size_t cx_strsplit_m(cxmutstr string, cxstring delim,
-        size_t limit, cxmutstr *output);
-
-/**
- * Splits a given string using a delimiter string.
- *
- * 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.
- *
- * @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
- * @param delim  the delimiter
- * @param limit the maximum number of split items
- * @param output a pointer where the address of the allocated array shall be
- * written to
- * @return the actual number of split items
- */
-cx_attr_nodiscard cx_attr_nonnull cx_attr_access_w(5)
-CX_EXPORT size_t cx_strsplit_ma(const CxAllocator *allocator,
-        cxmutstr string, cxstring delim, size_t limit,
-        cxmutstr **output);
+#define cx_strsplit_a(allocator, string, delim, limit, output) \
+        _Generic(cx_strcast_m(string), \
+        cxstring: cx_strsplit_a_, \
+        cxmutstr: cx_strsplit_ma_)\
+        (allocator, cx_strcast_m(string), cx_strcast(delim), limit, output)
+#endif
 
 /**
  * Compares two strings.
@@ -753,8 +970,8 @@
  * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
  * than @p s2, zero if both strings equal
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_strcmp_(cxstring s1, cxstring s2);
+CX_EXTERN CX_NODISCARD
+int cx_strcmp_(cxstring s1, cxstring s2);
 
 /**
  * Compares two strings.
@@ -774,8 +991,8 @@
  * @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
  */
-cx_attr_nodiscard
-CX_EXPORT int cx_strcasecmp_(cxstring s1, cxstring s2);
+CX_EXTERN CX_NODISCARD
+int cx_strcasecmp_(cxstring s1, cxstring s2);
 
 /**
  * Compares two strings ignoring case.
@@ -800,8 +1017,8 @@
  * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
  * than @p s2, zero if both strings equal
  */
-cx_attr_nodiscard  cx_attr_nonnull
-CX_EXPORT int cx_strcmp_p(const void *s1, const void *s2);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+int cx_strcmp_p(const void *s1, const void *s2);
 
 /**
  * Compares two strings ignoring case.
@@ -813,9 +1030,8 @@
  * @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
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT int cx_strcasecmp_p(const void *s1, const void *s2);
-
+CX_EXTERN CX_NODISCARD CX_NONNULL
+int cx_strcasecmp_p(const void *s1, const void *s2);
 
 /**
  * Creates a duplicate of the specified string.
@@ -829,8 +1045,8 @@
  * @return a duplicate of the string
  * @see cx_strdup()
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT cxmutstr cx_strdup_a_(const CxAllocator *allocator, cxstring string);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+cxmutstr cx_strdup_a_(const CxAllocator *allocator, cxstring string);
 
 /**
  * Creates a duplicate of the specified string.
@@ -863,16 +1079,31 @@
 #define cx_strdup(string) cx_strdup_a(cxDefaultAllocator, string)
 
 /**
- * Omits leading and trailing spaces.
- *
- * @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
+ * Trims a string.
+ * Internal function - do not use.
+ * @param string
+ * @return
  */
-cx_attr_nodiscard
-CX_EXPORT cxstring cx_strtrim(cxstring string);
+CX_EXTERN CX_NODISCARD
+cxstring cx_strtrim_(cxstring string);
+
+#ifdef __cplusplus
+CX_CPPDECL cxstring cx_strtrim_cpp_(cxstring string) {
+    return cx_strtrim_(string);
+}
+CX_CPPDECL cxmutstr cx_strtrim_cpp_(cxmutstr string) {
+    return cx_mutstrcast(cx_strtrim_(cx_strcast(string)));
+}
+#define cx_strtrim(string) cx_strtrim_cpp_(cx_strcast_m(string))
+#else
+/**
+ * Internal conversion function.
+ * @param string
+ * @return
+ */
+CX_INLINE cxmutstr cx_strtrim_m_(cxmutstr string) {
+    return cx_mutstrcast(cx_strtrim_(cx_strcast(string)));
+}
 
 /**
  * Omits leading and trailing spaces.
@@ -881,10 +1112,12 @@
  * must @em not free the returned memory.
  *
  * @param string the string that shall be trimmed
- * @return the trimmed string
+ * @return (@c cxstring or @c cxmutstr) the trimmed string
  */
-cx_attr_nodiscard
-CX_EXPORT cxmutstr cx_strtrim_m(cxmutstr string);
+#define cx_strtrim(string) _Generic(cx_strcast_m(string), \
+        cxstring: cx_strtrim_, \
+        cxmutstr: cx_strtrim_m_)(cx_strcast_m(string))
+#endif
 
 /**
  * Checks if a string has a specific prefix.
@@ -894,8 +1127,8 @@
  * @return @c true, if and only if the string has the specified prefix,
  * @c false otherwise
  */
-cx_attr_nodiscard
-CX_EXPORT bool cx_strprefix_(cxstring string, cxstring prefix);
+CX_EXTERN CX_NODISCARD
+bool cx_strprefix_(cxstring string, cxstring prefix);
 
 /**
  * Checks if a string has a specific prefix.
@@ -915,8 +1148,8 @@
  * @return @c true, if and only if the string has the specified suffix,
  * @c false otherwise
  */
-cx_attr_nodiscard
-CX_EXPORT bool cx_strsuffix_(cxstring string, cxstring suffix);
+CX_EXTERN CX_NODISCARD
+bool cx_strsuffix_(cxstring string, cxstring suffix);
 
 /**
  * Checks if a string has a specific suffix.
@@ -936,8 +1169,8 @@
  * @return @c true, if and only if the string has the specified prefix,
  * @c false otherwise
  */
-cx_attr_nodiscard
-CX_EXPORT bool cx_strcaseprefix_(cxstring string, cxstring prefix);
+CX_EXTERN CX_NODISCARD
+bool cx_strcaseprefix_(cxstring string, cxstring prefix);
 
 /**
  * Checks if a string has a specific prefix, ignoring the case.
@@ -957,8 +1190,8 @@
  * @return @c true, if and only if the string has the specified suffix,
  * @c false otherwise
  */
-cx_attr_nodiscard
-CX_EXPORT bool cx_strcasesuffix_(cxstring string, cxstring suffix);
+CX_EXTERN CX_NODISCARD
+bool cx_strcasesuffix_(cxstring string, cxstring suffix);
 
 /**
  * Checks, if a string has a specific suffix, ignoring the case.
@@ -973,6 +1206,26 @@
 /**
  * Replaces a string with another string.
  *
+ * Internal function - do not use.
+ *
+ * @param allocator
+ * @param str
+ * @param search
+ * @param replacement
+ * @param replmax
+ * @return
+ * @see cx_strreplace_a()
+ * @see cx_strreplace()
+ * @see cx_strreplacen_a()
+ * @see cx_strreplacen()
+ */
+CX_EXTERN CX_NODISCARD CX_NONNULL
+cxmutstr cx_strreplace_(const CxAllocator *allocator,
+        cxstring str, cxstring search, cxstring replacement, size_t replmax);
+
+/**
+ * Replaces a string with another string.
+ *
  * The function replaces at most @p replmax occurrences.
  *
  * The returned string will be allocated by @p allocator and is guaranteed
@@ -981,16 +1234,15 @@
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param allocator the allocator to use
+ * @param allocator (@c CxAllocator*) the allocator to use
  * @param str the string where replacements should be applied
  * @param search the string to search for
  * @param replacement the replacement string
- * @param replmax maximum number of replacements
- * @return the resulting string after applying the replacements
+ * @param replmax (@c size_t) maximum number of replacements
+ * @return (@c cxmutstr) the resulting string after applying the replacements
  */
-cx_attr_nodiscard cx_attr_nonnull
-CX_EXPORT cxmutstr cx_strreplacen_a(const CxAllocator *allocator,
-        cxstring str, cxstring search, cxstring replacement, size_t replmax);
+#define cx_strreplacen_a(allocator, str, search, replacement, replmax) \
+        cx_strreplace_(allocator, cx_strcast(str), cx_strcast(search), cx_strcast(replacement), replmax)
 
 /**
  * Replaces a string with another string.
@@ -1003,9 +1255,9 @@
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param str (@c cxstring) the string where replacements should be applied
- * @param search (@c cxstring) the string to search for
- * @param replacement (@c cxstring) the replacement string
+ * @param str the string where replacements should be applied
+ * @param search the string to search for
+ * @param replacement the replacement string
  * @param replmax (@c size_t) maximum number of replacements
  * @return (@c cxmutstr) the resulting string after applying the replacements
  */
@@ -1022,9 +1274,9 @@
  * the returned string will be empty.
  *
  * @param allocator (@c CxAllocator*) the allocator to use
- * @param str (@c cxstring) the string where replacements should be applied
- * @param search (@c cxstring) the string to search for
- * @param replacement (@c cxstring) the replacement string
+ * @param str the string where replacements should be applied
+ * @param search the string to search for
+ * @param replacement the replacement string
  * @return (@c cxmutstr) the resulting string after applying the replacements
  */
 #define cx_strreplace_a(allocator, str, search, replacement) \
@@ -1039,9 +1291,9 @@
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param str (@c cxstring) the string where replacements should be applied
- * @param search (@c cxstring) the string to search for
- * @param replacement (@c cxstring) the replacement string
+ * @param str the string where replacements should be applied
+ * @param search the string to search for
+ * @param replacement the replacement string
  * @return (@c cxmutstr) the resulting string after applying the replacements
  */
 #define cx_strreplace(str, search, replacement) \
@@ -1055,8 +1307,8 @@
  * @param limit the maximum number of tokens that shall be returned
  * @return a new string tokenization context
  */
-cx_attr_nodiscard
-CX_EXPORT CxStrtokCtx cx_strtok_(cxstring str, cxstring delim, size_t limit);
+CX_EXTERN CX_NODISCARD
+CxStrtokCtx cx_strtok_(cxstring str, cxstring delim, size_t limit);
 
 /**
  * Creates a string tokenization context.
@@ -1079,25 +1331,34 @@
  * @return true if successful, false if the limit or the end of the string
  * has been reached
  */
-cx_attr_nonnull  cx_attr_nodiscard  cx_attr_access_w(2)
-CX_EXPORT bool cx_strtok_next(CxStrtokCtx *ctx, cxstring *token);
+CX_EXTERN CX_NONNULL CX_NODISCARD CX_ACCESS_W(2)
+bool cx_strtok_next_(CxStrtokCtx *ctx, cxstring *token);
 
+#ifdef __cplusplus
+CX_CPPDECL cx_strtok_next(CxStrtokCtx *ctx, cxstring *token) {
+    return cx_strtok_next_(ctx, token);
+}
+CX_CPPDECL cx_strtok_next(CxStrtokCtx *ctx, cxmutstr *token) {
+    // Note: this is actually UB - fixed with start_lifetime_as() in C++23
+    //       but it works on all supported platforms
+    return cx_strtok_next_(ctx, reinterpret_cast<cxstring*>(token));
+}
+#else // ! __cplusplus
 /**
- * Returns the next token of a mutable string.
+ * Returns the next token.
  *
  * The token will point to the source string.
  *
- * @attention
- * If the context was not initialized over a mutable string, modifying
- * the data of the returned token is undefined behavior.
- *
- * @param ctx the tokenization context
- * @param token a pointer to memory where the next token shall be stored
+ * @param ctx (@c CxStrtokCtx*) the tokenization context
+ * @param token a pointer to either a @c cxstring or @c cxmutstr
+ * where the next token shall be stored
  * @return true if successful, false if the limit or the end of the string
  * has been reached
  */
-cx_attr_nonnull  cx_attr_nodiscard  cx_attr_access_w(2)
-CX_EXPORT bool cx_strtok_next_m(CxStrtokCtx *ctx, cxmutstr *token);
+#define cx_strtok_next(ctx, token) _Generic((token), \
+        cxstring*: cx_strtok_next_, \
+        cxmutstr*: cx_strtok_next_)(ctx, (cxstring*)token)
+#endif
 
 /**
  * Defines an array of more delimiters for the specified tokenization context.
@@ -1106,8 +1367,8 @@
  * @param delim array of more delimiters
  * @param count number of elements in the array
  */
-cx_attr_nonnull cx_attr_access_r(2, 3)
-CX_EXPORT void cx_strtok_delim(CxStrtokCtx *ctx, const cxstring *delim, size_t count);
+CX_EXTERN CX_NONNULL CX_ACCESS_R(2, 3)
+void cx_strtok_delim(CxStrtokCtx *ctx, const cxstring *delim, size_t count);
 
 /* ------------------------------------------------------------------------- *
  *                string to number conversion functions                      *
@@ -1127,8 +1388,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtos_lc_(cxstring str, short *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtos_lc_(cxstring str, short *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1144,8 +1405,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoi_lc_(cxstring str, int *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoi_lc_(cxstring str, int *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1161,8 +1422,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtol_lc_(cxstring str, long *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtol_lc_(cxstring str, long *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1178,8 +1439,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoll_lc_(cxstring str, long long *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoll_lc_(cxstring str, long long *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1195,8 +1456,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoi8_lc_(cxstring str, int8_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoi8_lc_(cxstring str, int8_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1212,8 +1473,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoi16_lc_(cxstring str, int16_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoi16_lc_(cxstring str, int16_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1229,8 +1490,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoi32_lc_(cxstring str, int32_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoi32_lc_(cxstring str, int32_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1246,8 +1507,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoi64_lc_(cxstring str, int64_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoi64_lc_(cxstring str, int64_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1263,8 +1524,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtous_lc_(cxstring str, unsigned short *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtous_lc_(cxstring str, unsigned short *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1280,8 +1541,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtou_lc_(cxstring str, unsigned int *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtou_lc_(cxstring str, unsigned int *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1297,8 +1558,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoul_lc_(cxstring str, unsigned long *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoul_lc_(cxstring str, unsigned long *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1314,8 +1575,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoull_lc_(cxstring str, unsigned long long *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoull_lc_(cxstring str, unsigned long long *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1331,8 +1592,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtou8_lc_(cxstring str, uint8_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtou8_lc_(cxstring str, uint8_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1348,8 +1609,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtou16_lc_(cxstring str, uint16_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtou16_lc_(cxstring str, uint16_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1365,8 +1626,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtou32_lc_(cxstring str, uint32_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtou32_lc_(cxstring str, uint32_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1382,8 +1643,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtou64_lc_(cxstring str, uint64_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtou64_lc_(cxstring str, uint64_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1399,8 +1660,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtoz_lc_(cxstring str, size_t *output, int base, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtoz_lc_(cxstring str, size_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a single precision floating-point number.
@@ -1416,8 +1677,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtof_lc_(cxstring str, float *output, char decsep, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_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.
@@ -1433,8 +1694,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-CX_EXPORT int cx_strtod_lc_(cxstring str, double *output, char decsep, const char *groupsep);
+CX_EXTERN CX_ACCESS_W(2) CX_NONNULL_ARG(2)
+int cx_strtod_lc_(cxstring str, double *output, char decsep, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -2080,8 +2341,4 @@
  */
 #define cx_strtod(str, output) cx_strtod_lc_(cx_strcast(str), output, '.', ",")
 
-#ifdef __cplusplus
-} // extern "C"
-#endif
-
-#endif //Ucx_strING_H
+#endif //UCX_STRING_H
--- a/ucx/cx/test.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/test.h	Wed Dec 31 16:40:12 2025 +0100
@@ -73,10 +73,6 @@
 #include <string.h>
 #include <setjmp.h>
 
-#ifdef	__cplusplus
-extern "C" {
-#endif
-
 #ifndef __FUNCTION__
 /**
  * Alias for the <code>__func__</code> preprocessor macro.
@@ -136,7 +132,7 @@
  * @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
+CX_NONNULL CX_NODISCARD  CX_CSTR_ARG(1) CX_MALLOC
 static inline CxTestSuite* cx_test_suite_new(const char *name) {
     CxTestSuite* suite = (CxTestSuite*) malloc(sizeof(CxTestSuite));
     if (suite != NULL) {
@@ -173,7 +169,7 @@
  * @retval zero success
  * @retval non-zero failure
  */
-cx_attr_nonnull
+CX_NONNULL
 CX_INLINE int cx_test_register(CxTestSuite* suite, CxTest test) {
     CxTestSet *t = (CxTestSet*) malloc(sizeof(CxTestSet));
     if (t) {
@@ -200,7 +196,7 @@
  * @param out_target the target buffer or file to write the output to
  * @param out_writer the write function writing to @p out_target
  */
-cx_attr_nonnull
+CX_NONNULL
 CX_INLINE void cx_test_run(CxTestSuite *suite, void *out_target, cx_write_func out_writer) {
     if (suite->name == NULL) {
         out_writer("*** Test Suite ***\n", 1, 19, out_target);
@@ -218,7 +214,7 @@
     char total[80];
     int len = snprintf(
             total, 80,
-            "  Total:   %u\n  Success: %u\n  Failure: %u\n\n",
+            "  Tests:   %5u\n  Success: %5u\n  Failure: %5u\n\n",
             suite->success + suite->failure, suite->success, suite->failure
     );
     out_writer(total, 1, len, out_target);
@@ -326,9 +322,5 @@
 #define CX_TEST_CALL_SUBROUTINE(name,...) \
         name(_suite_,_output_,_writefnc_,_env_,__VA_ARGS__)
 
-#ifdef	__cplusplus
-}
-#endif
-
 #endif	/* UCX_TEST_H */
 
--- a/ucx/cx/tree.h	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/cx/tree.h	Wed Dec 31 16:40:12 2025 +0100
@@ -40,93 +40,6 @@
 
 #include "collection.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/**
- * A depth-first tree iterator.
- *
- * This iterator is not position-aware in a strict sense, as it does not assume
- * a particular order of elements in the tree. However, the iterator keeps track
- * of the number of nodes it has passed in a counter-variable.
- * Each node, regardless of the number of passes, is counted only once.
- *
- * @note Objects that are pointed to by an iterator are mutable through that
- * iterator. However, if the
- * underlying data structure is mutated by other means than this iterator (e.g.,
- * elements added or removed), the iterator becomes invalid (regardless of what
- * cxIteratorValid() returns).
- *
- * @see CxIterator
- */
-typedef struct cx_tree_iterator_s {
-    /**
-     * Base members.
-     */
-    CX_ITERATOR_BASE;
-    /**
-     * Indicates whether the subtree below the current node shall be skipped.
-     */
-    bool skip;
-    /**
-     * Set to true, when the iterator shall visit a node again
-     * when all its children have been processed.
-     */
-    bool visit_on_exit;
-    /**
-     * True, if this iterator is currently leaving the node.
-     */
-    bool exiting;
-    /**
-     * Offset in the node struct for the children linked list.
-     */
-    ptrdiff_t loc_children;
-    /**
-     * Offset in the node struct for the next pointer.
-     */
-    ptrdiff_t loc_next;
-    /**
-     * The total number of distinct nodes that have been passed so far.
-     * This includes the current node.
-     */
-    size_t counter;
-    /**
-     * The currently observed node.
-     *
-     * This is the same what cxIteratorCurrent() would return.
-     */
-    void *node;
-    /**
-     * Stores a copy of the pointer to the successor of the visited node.
-     * Allows freeing a node on exit without corrupting the iteration.
-     */
-    void *node_next;
-    /**
-     * Internal stack.
-     * Will be automatically freed once the iterator becomes invalid.
-     *
-     * If you want to discard the iterator before, you need to manually
-     * call cxTreeIteratorDispose().
-     */
-    void **stack;
-    /**
-     * Internal capacity of the stack.
-     */
-    size_t stack_capacity;
-    union {
-        /**
-         * Internal stack size.
-         */
-        size_t stack_size;
-        /**
-         * The current depth in the tree.
-         * The node with which the iteration starts has depth 1.
-         */
-        size_t depth;
-    };
-} CxTreeIterator;
-
 /**
  * An element in a visitor queue.
  */
@@ -147,36 +60,14 @@
 };
 
 /**
- * A breadth-first tree iterator.
- *
- * This iterator needs to maintain a visitor queue that will be automatically
- * freed once the iterator becomes invalid.
- * If you want to discard the iterator before, you MUST manually call
- * cxTreeVisitorDispose().
- *
- * This iterator is not position-aware in a strict sense, as it does not assume
- * a particular order of elements in the tree. However, the iterator keeps track
- * of the number of nodes it has passed in a counter-variable.
- * Each node, regardless of the number of passes, is counted only once.
- *
- * @note Objects that are pointed to by an iterator are mutable through that
- * iterator. However, if the
- * underlying data structure is mutated by other means than this iterator (e.g.,
- * elements added or removed), the iterator becomes invalid (regardless of what
- * cxIteratorValid() returns).
- *
- * @see CxIterator
+ * An iterator (DFS) or visitor (BFS) for a tree.
  */
-typedef struct cx_tree_visitor_s {
+typedef struct cx_tree_combined_iterator_s {
     /**
      * Base members.
      */
     CX_ITERATOR_BASE;
     /**
-     * Indicates whether the subtree below the current node shall be skipped.
-     */
-    bool skip;
-    /**
      * Offset in the node struct for the children linked list.
      */
     ptrdiff_t loc_children;
@@ -190,38 +81,74 @@
      */
     size_t counter;
     /**
+     * The current depth in the tree.
+     */
+    size_t depth;
+    /**
      * The currently observed node.
      *
      * This is the same what cxIteratorCurrent() would return.
      */
     void *node;
     /**
-     * The current depth in the tree.
-     */
-    size_t depth;
-    /**
-     * The next element in the visitor queue.
+     * Memory for BFS or DFS-specific data.
      */
-    struct cx_tree_visitor_queue_s *queue_next;
+    union {
+        struct {
+            /**
+             * Stores a copy of the pointer to the successor of the visited node.
+             * Allows freeing a node on exit without corrupting the iteration.
+             */
+            void *node_next;
+            /**
+             * Internal stack.
+             * Will be automatically freed once the iterator becomes invalid.
+             *
+             * If you want to discard the iterator before, you need to manually
+             * call cxTreeIteratorDispose().
+             */
+            void **stack;
+            /**
+             * Internal capacity of the stack.
+             */
+            size_t stack_capacity;
+        };
+        struct {
+            /**
+             * The next element in the visitor queue.
+             */
+            struct cx_tree_visitor_queue_s *queue_next;
+            /**
+             * The last element in the visitor queue.
+             */
+            struct cx_tree_visitor_queue_s *queue_last;
+        };
+    };
     /**
-     * The last element in the visitor queue.
+     * Indicates whether the subtree below the current node shall be skipped.
+     */
+    bool skip;
+    /**
+     * Set to true, when the iterator shall visit a node again
+     * when all its children have been processed.
      */
-    struct cx_tree_visitor_queue_s *queue_last;
-} CxTreeVisitor;
+    bool visit_on_exit;
+    /**
+     * True, if this iterator is currently leaving the node.
+     */
+    bool exiting;
+    /**
+     * Indicates whether the @c iterator (true) or the @c visitor (false) aspect is active.
+     */
+    bool use_dfs;
+} CxTreeIterator;
 
 /**
  * Releases internal memory of the given tree iterator.
  * @param iter the iterator
  */
-cx_attr_nonnull
-CX_EXPORT void cxTreeIteratorDispose(CxTreeIterator *iter);
-
-/**
- * Releases internal memory of the given tree visitor.
- * @param visitor the visitor
- */
-cx_attr_nonnull
-CX_EXPORT void cxTreeVisitorDispose(CxTreeVisitor *visitor);
+CX_EXTERN CX_NONNULL
+void cxTreeIteratorDispose(CxTreeIterator *iter);
 
 /**
  * Advises the iterator to skip the subtree below the current node and
@@ -232,14 +159,6 @@
 #define cxTreeIteratorContinue(iterator) (iterator).skip = true; continue
 
 /**
- * Advises the visitor to skip the subtree below the current node and
- * also continues the current loop.
- *
- * @param visitor (@c CxTreeVisitor) the visitor
- */
-#define cxTreeVisitorContinue(visitor) cxTreeIteratorContinue(visitor)
-
-/**
  * Links a node to a (new) parent.
  *
  * If the node already has a parent, it is unlinked, first.
@@ -254,10 +173,10 @@
  * the last child in the linked list (negative if there is no such 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()
+ * @see cx_tree_remove()
  */
-cx_attr_nonnull
-CX_EXPORT void cx_tree_link(void *parent, void *node,
+CX_EXTERN CX_NONNULL
+void cx_tree_add(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);
 
@@ -273,10 +192,10 @@
  * the last child in the linked list (negative if there is no such 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()
+ * @see cx_tree_add()
  */
-cx_attr_nonnull
-CX_EXPORT void cx_tree_unlink(void *node,
+CX_EXTERN CX_NONNULL
+void cx_tree_remove(void *node,
         ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
         ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
@@ -311,43 +230,14 @@
  * positive if one of the children might contain the data,
  * negative if neither the node nor the children contains the data
  */
-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
- * 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, consider a tree that stores file path information.
- * A node which is describing a parent directory of a searched file shall
- * return a positive number to indicate that a child node might contain the
- * searched item. On the other hand, if the node denotes a path that is not a
- * prefix of the searched filename, the function would return -1 to indicate
- * that the search does not need to be continued in that branch.
- *
- * @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,
- * positive if one of the children might contain the data,
- * negative if neither the node nor the children contains the data
- */
-typedef int (*cx_tree_search_func)(const void *node, const void *new_node);
+typedef int (*cx_tree_search_func)(const void *node, const void *data);
 
 /**
  * Searches for data in a tree.
  *
  * When the data cannot be found exactly, the search function might return the
  * closest result, which might be a good starting point for adding a new node
- * to the tree (see also #cx_tree_add()).
+ * to the tree.
  *
  * Depending on the tree structure, it is not necessarily guaranteed that the
  * "closest" match is uniquely defined. This function will search for a node
@@ -356,7 +246,7 @@
  * node matching the criteria is returned.
  *
  * @param root the root node
- * @param depth the maximum depth (zero=indefinite, one=just root)
+ * @param max_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
@@ -366,39 +256,10 @@
  * 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
  */
-cx_attr_nonnull cx_attr_access_w(5)
-CX_EXPORT int cx_tree_search_data(const void *root, size_t depth,
-        const void *data, cx_tree_search_data_func sfunc,
-        void **result, ptrdiff_t loc_children, ptrdiff_t loc_next);
-
-/**
- * Searches for a node in a tree.
- *
- * When no node with the same data can be found, the search function might
- * return the closest result, which might be a good starting point for adding the
- * new node to the tree (see also #cx_tree_add()).
- *
- * 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
- * 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
- * @param loc_children offset in the node struct for the children linked list
- * @param loc_next offset in the node struct for the next pointer
- * @return zero if the node was found exactly, positive if a node was found that
- * 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
- */
-cx_attr_nonnull cx_attr_access_w(5)
-CX_EXPORT 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);
+CX_EXTERN CX_NONNULL_ARG(4, 5) CX_ACCESS_W(5)
+int cx_tree_search(const void *root, size_t max_depth,
+        const void *data, cx_tree_search_func sfunc, void **result,
+        ptrdiff_t loc_children, ptrdiff_t loc_next);
 
 /**
  * Creates a depth-first iterator for a tree with the specified root node.
@@ -420,8 +281,8 @@
  * @return the new tree iterator
  * @see cxTreeIteratorDispose()
  */
-cx_attr_nodiscard
-CX_EXPORT CxTreeIterator cx_tree_iterator(void *root, bool visit_on_exit,
+CX_EXTERN CX_NODISCARD
+CxTreeIterator cx_tree_iterator(void *root, bool visit_on_exit,
         ptrdiff_t loc_children, ptrdiff_t loc_next);
 
 /**
@@ -431,7 +292,7 @@
  * is allocated using the cxDefaultAllocator.
  * When the visitor becomes invalid, this memory is automatically released.
  * However, if you wish to cancel the iteration before the visitor becomes
- * invalid by itself, you MUST call cxTreeVisitorDispose() manually to release
+ * invalid by itself, you MUST call cxTreeIteratorDispose() manually to release
  * the memory.
  *
  * @remark The returned iterator does not support cxIteratorFlagRemoval().
@@ -440,178 +301,11 @@
  * @param loc_children offset in the node struct for the children linked list
  * @param loc_next offset in the node struct for the next pointer
  * @return the new tree visitor
- * @see cxTreeVisitorDispose()
- */
-cx_attr_nodiscard
-CX_EXPORT CxTreeVisitor cx_tree_visitor(void *root,
-        ptrdiff_t loc_children, ptrdiff_t loc_next);
-
-/**
- * Describes a function that creates a tree node from the specified data.
- * 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.
- *
- * @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.
- */
-typedef void *(*cx_tree_node_create_func)(const void *, void *);
-
-/**
- * The local search depth for a new subtree when adding multiple elements.
- * The default value is 3.
- * This variable is used by #cx_tree_add_array() and #cx_tree_add_iter() to
- * implement optimized insertion of multiple elements into a tree.
- */
-CX_EXPORT extern unsigned int cx_tree_add_look_around_depth;
-
-/**
- * Adds multiple elements efficiently to a tree.
- *
- * 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.
- * 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.
- *
- * The advantage of this function compared to multiple invocations of
- * #cx_tree_add() is that the search for the insert locations is not always
- * started from the root node.
- * Instead, the function checks #cx_tree_add_look_around_depth many parent nodes
- * of the current insert location before starting from the root node again.
- * When the variable is set to zero, only the last found location is checked
- * again.
- *
- * Refer to the documentation of #cx_tree_add() for more details.
- *
- * @param iter a pointer to an arbitrary iterator
- * @param num the maximum number of elements to obtain from the iterator
- * @param sfunc a search function
- * @param cfunc a node creation function
- * @param cdata optional additional data
- * @param root the root node of the tree
- * @param failed location where the pointer to a failed node shall be stored
- * @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 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()
+ * @see cxTreeIteratorDispose()
  */
-cx_attr_nonnull_arg(1, 3, 4, 6, 7) cx_attr_access_w(6)
-CX_EXPORT size_t cx_tree_add_iter(struct cx_iterator_base_s *iter, size_t num,
-        cx_tree_search_func sfunc, cx_tree_node_create_func cfunc,
-        void *cdata, void **failed, void *root,
-        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev, ptrdiff_t loc_next);
-
-/**
- * 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 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 advantage of this function compared to multiple invocations of
- * #cx_tree_add() is that the search for the insert locations is not always
- * started from the root node.
- * Instead, the function checks #cx_tree_add_look_around_depth many parent nodes
- * of the current insert location before starting from the root node again.
- * When the variable is set to zero, only the last found location is checked
- * again.
- *
- * 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 sfunc a search function
- * @param cfunc a node creation function
- * @param cdata optional additional data
- * @param failed location where the pointer to a failed node shall be stored
- * @param root the root node of the tree
- * @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 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()
- */
-cx_attr_nonnull_arg(1, 4, 5, 7, 8) cx_attr_access_w(7)
-CX_EXPORT size_t cx_tree_add_array(const void *src, size_t num, size_t elem_size,
-        cx_tree_search_func sfunc, cx_tree_node_create_func cfunc,
-        void *cdata, void **failed, void *root,
-        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev, ptrdiff_t loc_next);
-
-/**
- * Adds data to a tree.
- *
- * An adequate location where to add the new tree node is searched with the
- * specified @p sfunc.
- *
- * 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 returns 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
- * accordingly.
- *
- * When @p sfunc returns zero and the found node has a parent, the new
- * node will be added as a sibling - otherwise, the new node will be added
- * as a child.
- *
- * When @p sfunc returns 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
- * 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.
- *
- * Multiple elements can be added more efficiently with
- * #cx_tree_add_array() or #cx_tree_add_iter().
- *
- * @param src a pointer to the data
- * @param sfunc a search function
- * @param cfunc a node creation function
- * @param cdata optional additional data
- * @param cnode the location where a pointer to the new node is stored
- * @param root the root node of the tree
- * @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 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
- */
-cx_attr_nonnull_arg(1, 2, 3, 5, 6) cx_attr_access_w(5)
-CX_EXPORT int cx_tree_add(const void *src,
-        cx_tree_search_func sfunc, cx_tree_node_create_func cfunc,
-        void *cdata, void **cnode, void *root,
-        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev, ptrdiff_t loc_next);
-
-
-/**
- * Tree class type.
- */
-typedef struct cx_tree_class_s cx_tree_class;
+CX_EXTERN CX_NODISCARD
+CxTreeIterator cx_tree_visitor(void *root,
+        ptrdiff_t loc_children, ptrdiff_t loc_next);
 
 /**
  * Base structure that can be used for tree nodes in a #CxTree.
@@ -642,14 +336,10 @@
 /**
  * Structure for holding the base data of a tree.
  */
-struct cx_tree_s {
+typedef struct cx_tree_s {
+    /** Base attributes. */
     CX_COLLECTION_BASE;
     /**
-     * The tree class definition.
-     */
-    const cx_tree_class *cl;
-
-    /**
      * A pointer to the root node.
      *
      * Will be @c NULL when @c size is 0.
@@ -657,24 +347,9 @@
     void *root;
 
     /**
-     * A function to create new nodes.
-     *
-     * Invocations to this function will receive a pointer to this tree
-     * structure as the second argument.
-     *
-     * Nodes MAY use #cx_tree_node_base_s as the base layout, but do not need to.
+     * The size of the node structure.
      */
-    cx_tree_node_create_func node_create;
-
-    /**
-     * A function to compare two nodes.
-     */
-    cx_tree_search_func search;
-
-    /**
-     * A function to compare a node with data.
-     */
-    cx_tree_search_data_func search_data;
+    size_t node_size;
 
     /**
      * Offset in the node struct for the parent pointer.
@@ -701,7 +376,12 @@
      * Offset in the node struct for the next sibling pointer.
      */
     ptrdiff_t loc_next;
-};
+
+    /**
+     * Offset in the node struct where the payload is located.
+     */
+    ptrdiff_t loc_data;
+} CxTree;
 
 /**
  * Macro to roll out the #cx_tree_node_base_s structure with a custom
@@ -711,7 +391,7 @@
  *
  * @param type the data type for the nodes
  */
-#define CX_TREE_NODE_BASE(type) \
+#define CX_TREE_NODE(type) \
     type *parent; \
     type *children;\
     type *last_child;\
@@ -719,51 +399,20 @@
     type *next
 
 /**
- * Macro for specifying the layout of a base node tree.
+ * Macro for specifying the layout of a tree node.
  *
- * When your tree uses #CX_TREE_NODE_BASE, you can use this
+ * When your tree uses #CX_TREE_NODE, 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),\
-    offsetof(struct cx_tree_node_base_s, children),\
-    offsetof(struct cx_tree_node_base_s, last_child),\
-    offsetof(struct cx_tree_node_base_s, prev),  \
-    offsetof(struct cx_tree_node_base_s, next)
-
-/**
- * The class definition for arbitrary trees.
+ * @param struct_name the name of the node structure
  */
-struct cx_tree_class_s {
-    /**
-     * Member function for inserting a single element.
-     *
-     * Implementations SHALL NOT simply invoke @p insert_many as this comes
-     * with too much overhead.
-     */
-    int (*insert_element)(struct cx_tree_s *tree, const void *data);
-
-    /**
-     * Member function for inserting multiple elements.
-     *
-     * Implementations SHALL avoid performing a full search in the tree for
-     * every element even though the source data MAY be unsorted.
-     */
-    size_t (*insert_many)(struct cx_tree_s *tree, struct cx_iterator_base_s *iter, size_t n);
-
-    /**
-     * Member function for finding a node.
-     */
-    void *(*find)(struct cx_tree_s *tree, const void *subtree, const void *data, size_t depth);
-};
-
-/**
- * Common type for all tree implementations.
- */
-typedef struct cx_tree_s CxTree;
-
+#define cx_tree_node_layout(struct_name) \
+    offsetof(struct_name, parent),\
+    offsetof(struct_name, children),\
+    offsetof(struct_name, last_child),\
+    offsetof(struct_name, prev),  \
+    offsetof(struct_name, next)
 
 /**
  * Destroys a node and its subtree.
@@ -783,11 +432,11 @@
  * and would therefore result in a double-free.
  *
  * @param tree the tree
- * @param node the node to remove
+ * @param node the node being the root of the subtree to remove
  * @see cxTreeFree()
  */
-cx_attr_nonnull
-CX_EXPORT void cxTreeDestroySubtree(CxTree *tree, void *node);
+CX_EXTERN CX_NONNULL
+void cxTreeDestroySubtree(CxTree *tree, void *node);
 
 
 /**
@@ -807,7 +456,12 @@
  * @param tree the tree
  * @see cxTreeDestroySubtree()
  */
-#define cxTreeClear(tree) cxTreeDestroySubtree(tree, tree->root)
+CX_INLINE
+void cxTreeClear(CxTree *tree) {
+    if (tree->root != NULL) {
+        cxTreeDestroySubtree(tree, tree->root);
+    }
+}
 
 /**
  * Deallocates the tree structure.
@@ -825,71 +479,29 @@
  *
  * @param tree the tree to free
  */
-CX_EXPORT 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.
- *
- * @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, the cxDefaultAllocator 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
- * @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 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()
- */
-cx_attr_nonnull_arg(2, 3, 4) cx_attr_nodiscard  cx_attr_malloc cx_attr_dealloc(cxTreeFree, 1)
-CX_EXPORT CxTree *cxTreeCreate(const CxAllocator *allocator, cx_tree_node_create_func create_func,
-        cx_tree_search_func search_func, cx_tree_search_data_func search_data_func,
-        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev, ptrdiff_t loc_next);
+CX_EXTERN
+void cxTreeFree(CxTree *tree);
 
 /**
- * Creates a new tree structure based on a default layout.
+ * Creates a new tree.
  *
- * Nodes created by @p create_func MUST contain #cx_tree_node_base_s as the 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
- * will free the nodes with the allocator's free() method.
+ * The specified @p allocator will be used for creating the tree struct
+ * @em and the nodes.
  *
- * @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()
- */
-#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.
+ * When you do not specify an existing @p root, the tree will be created
+ * empty. Additionally, cxFree() is registered as the advanced destructor for
+ * the tree so that all nodes that you will add to the tree are automatically
+ * freed together with the tree.
+ * When @p root is not @c NULL, no destructors are registered automatically.
  *
- * The specified @p allocator will be used for creating the tree struct.
- *
- * @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 afterward.
- *
- * @param allocator the allocator that was used for nodes of the wrapped tree
+ * @param allocator the allocator to use
  * (if @c NULL, the cxDefaultAllocator is assumed)
- * @param root the root node of the tree that shall be wrapped
+ * @param node_size the complete size of one node
+ * @param elem_size the size of the payload stored in the node
+ * (@c CX_STORE_POINTERS is also supported)
+ * @param root an optional already existing root node
+ * @param loc_data optional offset in the node struct for the payload
+ * (when negative, cxTreeAddData() is not supported)
  * @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
@@ -899,92 +511,91 @@
  * @return the new tree
  * @see cxTreeCreate()
  */
-cx_attr_nonnull_arg(2) cx_attr_nodiscard  cx_attr_malloc  cx_attr_dealloc(cxTreeFree, 1)
-CX_EXPORT CxTree *cxTreeCreateWrapped(const CxAllocator *allocator, void *root,
+CX_EXTERN CX_NODISCARD  CX_MALLOC  CX_DEALLOC(cxTreeFree, 1)
+CxTree *cxTreeCreate(const CxAllocator *allocator,
+        size_t node_size, size_t elem_size, void *root, ptrdiff_t loc_data,
         ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
         ptrdiff_t loc_prev, ptrdiff_t loc_next);
 
 /**
- * Inserts data into the tree.
- *
- * @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
- * @retval zero success
- * @retval non-zero failure
- */
-cx_attr_nonnull
-CX_EXPORT int cxTreeInsert(CxTree *tree, const void *data);
-
-/**
- * Inserts elements provided by an iterator efficiently into the tree.
- *
- * @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 iter the iterator providing the elements
- * @param n the maximum number of elements to insert
- * @return the number of elements that could be successfully inserted
- */
-cx_attr_nonnull
-CX_EXPORT size_t cxTreeInsertIter(CxTree *tree, CxIteratorBase *iter, size_t n);
-
-/**
- * Inserts an array of data efficiently into the tree.
- *
- * @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 array of data to insert
- * @param elem_size the size of each element in the array
- * @param n the number of elements in the array
- * @return the number of elements that could be successfully inserted
- */
-cx_attr_nonnull
-CX_EXPORT size_t cxTreeInsertArray(CxTree *tree, const void *data, size_t elem_size, size_t n);
-
-/**
- * Searches the data in the specified tree.
- *
- * @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
- */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT void *cxTreeFind(CxTree *tree, const void *data);
-
-/**
  * Searches the data in the specified subtree.
  *
  * 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.
+ * @note When @p subtree_root is not @c NULL and not part of the @p tree,
+ * the behavior is undefined.
+ *
+ * @attention When @p loc_data is not available, this function immediately returns
+ * @c NULL.
  *
- * @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 (@c NULL to start from root)
+ * @param max_depth the maximum search depth
+ * @param use_dfs @c true when depth-first search should be used;
+ * @c false when breadth-first search should be used
+ * @return the first matching node, or @c NULL when the data cannot be found
+ * @see cxTreeFind()
+ */
+CX_EXTERN CX_NONNULL_ARG(1, 2) CX_NODISCARD
+void *cxTreeFindInSubtree(CxTree *tree, const void *data, void *subtree_root,
+        size_t max_depth, bool use_dfs);
+
+/**
+ * Searches the data in the specified tree.
+ *
+ * @attention When @p loc_data is not available, this function immediately returns
+ * @c NULL.
  *
  * @param tree the tree
  * @param data the data to search for
- * @param subtree_root the node where to start
+ * @param use_dfs @c true when depth-first search should be used;
+ * @c false when breadth-first search should be used
+ * @return the first matching node, or @c NULL when the data cannot be found
+ * @see cxTreeFindInSubtree()
+ * @see cxTreeFindFast()
+ */
+CX_INLINE CX_NONNULL CX_NODISCARD
+void *cxTreeFind(CxTree *tree, const void *data, bool use_dfs) {
+    if (tree->root == NULL) return NULL;
+    return cxTreeFindInSubtree(tree, data, tree->root, 0, use_dfs);
+}
+
+/**
+ * Performs an efficient depth-first search in a branch of the specified tree.
+ *
+ * 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 @c NULL and not part of the @p tree,
+ * the behavior is undefined.
+ *
+ * @param tree the tree
+ * @param data the data to search for
+ * @param sfunc the search function
+ * @param subtree_root the node where to start (@c NULL to start from root)
  * @param max_depth the maximum search depth
  * @return the first matching node, or @c NULL when the data cannot be found
+ * @see cxTreeFindInSubtree()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT void *cxTreeFindInSubtree(CxTree *tree, const void *data, void *subtree_root, size_t max_depth);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+void *cxTreeFindFastInSubtree(CxTree *tree, const void *data,
+        cx_tree_search_func sfunc, void *subtree_root, size_t max_depth);
+
+/**
+ * Performs an efficient depth-first search in the tree.
+ *
+ * @param tree the tree
+ * @param data the data to search for
+ * @param sfunc the search function
+ * @return the first matching node, or @c NULL when the data cannot be found
+ * @see cxTreeFind()
+ */
+CX_INLINE CX_NONNULL CX_NODISCARD
+void *cxTreeFindFast(CxTree *tree, const void *data, cx_tree_search_func sfunc) {
+    return cxTreeFindFastInSubtree(tree, data, sfunc, tree->root, 0);
+}
 
 /**
  * Determines the size of the specified subtree.
@@ -993,8 +604,8 @@
  * @param subtree_root the root node of the subtree
  * @return the number of nodes in the specified subtree
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
 
 /**
  * Determines the depth of the specified subtree.
@@ -1003,8 +614,8 @@
  * @param subtree_root the root node of the subtree
  * @return the tree depth including the @p subtree_root
  */
-cx_attr_nonnull  cx_attr_nodiscard
-CX_EXPORT size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
 
 /**
  * Determines the size of the entire tree.
@@ -1012,8 +623,8 @@
  * @param tree the tree
  * @return the tree size, counting the root as one
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT size_t cxTreeSize(CxTree *tree);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+size_t cxTreeSize(CxTree *tree);
 
 /**
  * Determines the depth of the entire tree.
@@ -1021,8 +632,8 @@
  * @param tree the tree
  * @return the tree depth, counting the root as one
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT size_t cxTreeDepth(CxTree *tree);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+size_t cxTreeDepth(CxTree *tree);
 
 /**
  * Creates a depth-first iterator for the specified tree starting in @p node.
@@ -1036,8 +647,8 @@
  * @return a tree iterator (depth-first)
  * @see cxTreeVisit()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxTreeIterator cxTreeIterateSubtree(CxTree *tree, void *node, bool visit_on_exit);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxTreeIterator cxTreeIterateSubtree(CxTree *tree, void *node, bool visit_on_exit);
 
 /**
  * Creates a breadth-first iterator for the specified tree starting in @p node.
@@ -1049,8 +660,8 @@
  * @return a tree visitor (a.k.a. breadth-first iterator)
  * @see cxTreeIterate()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxTreeVisitor cxTreeVisitSubtree(CxTree *tree, void *node);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxTreeIterator cxTreeVisitSubtree(CxTree *tree, void *node);
 
 /**
  * Creates a depth-first iterator for the specified tree.
@@ -1061,8 +672,8 @@
  * @return a tree iterator (depth-first)
  * @see cxTreeVisit()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxTreeIterator cxTreeIterate(CxTree *tree, bool visit_on_exit);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxTreeIterator cxTreeIterate(CxTree *tree, bool visit_on_exit);
 
 /**
  * Creates a breadth-first iterator for the specified tree.
@@ -1071,22 +682,22 @@
  * @return a tree visitor (a.k.a. breadth-first iterator)
  * @see cxTreeIterate()
  */
-cx_attr_nonnull cx_attr_nodiscard
-CX_EXPORT CxTreeVisitor cxTreeVisit(CxTree *tree);
+CX_EXTERN CX_NONNULL CX_NODISCARD
+CxTreeIterator cxTreeVisit(CxTree *tree);
 
 /**
  * Sets the (new) parent of the specified child.
  *
  * If the @p child is not already a member of the tree, this function behaves
- * as #cxTreeAddChildNode().
+ * as #cxTreeAddNode().
  *
  * @param tree the tree
  * @param parent the (new) parent of the child
  * @param child the node to add
- * @see cxTreeAddChildNode()
+ * @see cxTreeAddNode()
  */
-cx_attr_nonnull
-CX_EXPORT void cxTreeSetParent(CxTree *tree, void *parent, void *child);
+CX_EXTERN CX_NONNULL
+void cxTreeSetParent(CxTree *tree, void *parent, void *child);
 
 /**
  * Adds a new node to the tree.
@@ -1095,36 +706,81 @@
  * 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).
+ * as if it was created by the tree itself with #cxTreeAddData() (e.g., use
+ * the tree's allocator).
  *
  * @param tree the tree
  * @param parent the parent of the node to add
  * @param child the node to add
  * @see cxTreeSetParent()
+ * @see cxTreeAddData()
  */
-cx_attr_nonnull
-CX_EXPORT void cxTreeAddChildNode(CxTree *tree, void *parent, void *child);
+CX_EXTERN CX_NONNULL
+void cxTreeAddNode(CxTree *tree, void *parent, void *child);
+
+/**
+ * Creates a new node and adds it to the tree.
+ *
+ * @param tree the tree
+ * @param parent the parent node of the new node
+ * @param data the data that will be submitted to the create function
+ * @return the added node or @c NULL when the allocation failed
+ * @see cxTreeAdd()
+ */
+CX_EXTERN CX_NONNULL
+void *cxTreeAddData(CxTree *tree, void *parent, const void *data);
 
 /**
  * Creates a new node and adds it to the tree.
  *
- * With this function you can decide where exactly the new node shall be added.
- * If you specified an appropriate search function, you may want to consider
- * leaving this task to the tree by using #cxTreeInsert().
+ * @param tree the tree
+ * @param parent the parent node of the new node
+ * @return the added node or @c NULL when the allocation failed
+ */
+CX_EXTERN CX_NODISCARD CX_NONNULL
+void *cxTreeCreateNode(CxTree *tree, void *parent);
+
+/**
+ * Creates a new root node or returns the existing root node.
  *
- * Be aware that adding nodes at arbitrary locations in the tree might cause
- * wrong or undesired results when subsequently invoking #cxTreeInsert(), and
- * the invariant imposed by the search function does not hold any longer.
+ * @param tree the tree
+ * @return the new root node, the existing root node, or @c NULL when the allocation failed
+ * @see cxTreeCreateRootData()
+ */
+CX_EXTERN CX_NODISCARD CX_NONNULL
+void *cxTreeCreateRoot(CxTree *tree);
+
+/**
+ * Creates a new root node or uses the existing root node and writes the specified data to that node.
+ *
+ * @note This function immediately returns @c NULL when @c loc_data was not initialized with a positive value.
  *
  * @param tree the tree
- * @param parent the parent node of the new node
- * @param data the data that will be submitted to the create function
- * @return zero when the new node was created, non-zero on allocation failure
- * @see cxTreeInsert()
+ * @param data the data for the root node
+ * @return the new root node, the existing root node,
+ * or @c NULL when the allocation failed or the data location is not known
+ * @see cxTreeCreateRoot()
  */
-cx_attr_nonnull
-CX_EXPORT int cxTreeAddChild(CxTree *tree, void *parent, const void *data);
+CX_EXTERN CX_NODISCARD CX_NONNULL
+void *cxTreeCreateRootData(CxTree *tree, const void *data);
+
+/**
+ * Exchanges the root of the tree.
+ *
+ * @attention The old tree nodes might need to be deallocated by the caller.
+ * On the other hand, when the tree has destructors registered, keep in mind
+ * that they will be applied to the new tree.
+ * In particular, using cxTreeCreate() with a @c NULL root and setting the
+ * root with this function is @em not equivalent to creating the tree with
+ * a reference to an existing root because trees created without a root will
+ * have destructors registered.
+ *
+ * @param tree the tree
+ * @param new_root the new root node
+ * @return the old root node (or @c NULL when the tree was empty)
+ */
+CX_EXTERN CX_NONNULL_ARG(1) CX_NODISCARD
+void *cxTreeSetRoot(CxTree *tree, void *new_root);
 
 /**
  * A function that is invoked when a node needs to be re-linked to a new parent.
@@ -1158,8 +814,8 @@
  * node
  * @return zero on success, non-zero if @p node is the root node of the tree
  */
-cx_attr_nonnull_arg(1, 2)
-CX_EXPORT int cxTreeRemoveNode(CxTree *tree, void *node, cx_tree_relink_func relink_func);
+CX_EXTERN CX_NONNULL_ARG(1, 2)
+int cxTreeRemoveNode(CxTree *tree, void *node, cx_tree_relink_func relink_func);
 
 /**
  * Removes a node and its subtree from the tree.
@@ -1172,8 +828,8 @@
  * @param tree the tree
  * @param node the node to remove
  */
-cx_attr_nonnull
-CX_EXPORT void cxTreeRemoveSubtree(CxTree *tree, void *node);
+CX_EXTERN CX_NONNULL
+void cxTreeRemoveSubtree(CxTree *tree, void *node);
 
 /**
  * Destroys a node and re-links its children to its former parent.
@@ -1193,11 +849,7 @@
  * node
  * @return zero on success, non-zero if @p node is the root node of the tree
  */
-cx_attr_nonnull_arg(1, 2)
-CX_EXPORT int cxTreeDestroyNode(CxTree *tree, void *node, cx_tree_relink_func relink_func);
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
+CX_EXTERN CX_NONNULL_ARG(1, 2)
+int cxTreeDestroyNode(CxTree *tree, void *node, cx_tree_relink_func relink_func);
 
 #endif //UCX_TREE_H
--- a/ucx/hash_key.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/hash_key.c	Wed Dec 31 16:40:12 2025 +0100
@@ -63,14 +63,14 @@
     switch (len) {
         case 3:
             h ^= (data[i + 2] & 0xFF) << 16;
-            cx_attr_fallthrough;
+            CX_FALLTHROUGH;
         case 2:
             h ^= (data[i + 1] & 0xFF) << 8;
-            cx_attr_fallthrough;
+            CX_FALLTHROUGH;
         case 1:
             h ^= (data[i + 0] & 0xFF);
             h *= m;
-            cx_attr_fallthrough;
+            CX_FALLTHROUGH;
         default: // do nothing
             ;
     }
@@ -82,56 +82,6 @@
     key->hash = h;
 }
 
-
-uint32_t cx_hash_u32(uint32_t x) {
-    x = ((x >> 16) ^ x) * 0x45d9f3bu;
-    x = ((x >> 16) ^ x) * 0x45d9f3bu;
-    x = (x >> 16) ^ x;
-    return x;
-}
-
-uint64_t cx_hash_u64(uint64_t x) {
-    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
-    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
-    x = x ^ (x >> 31);
-    return x;
-}
-
-CxHashKey cx_hash_key_str(const char *str) {
-    CxHashKey key;
-    key.data = str;
-    key.len = str == NULL ? 0 : strlen(str);
-    cx_hash_murmur(&key);
-    return key;
-}
-
-CxHashKey cx_hash_key_ustr(unsigned const char *str) {
-    CxHashKey key;
-    key.data = str;
-    key.len = str == NULL ? 0 : strlen((const char*)str);
-    cx_hash_murmur(&key);
-    return key;
-}
-
-CxHashKey cx_hash_key_cxstr(cxstring str) {
-    return cx_hash_key(str.ptr, str.length);
-}
-
-CxHashKey cx_hash_key_mutstr(cxmutstr str) {
-    return cx_hash_key(str.ptr, str.length);
-}
-
-CxHashKey cx_hash_key_bytes(
-        const unsigned char *bytes,
-        size_t len
-) {
-    CxHashKey key;
-    key.data = bytes;
-    key.len = len;
-    cx_hash_murmur(&key);
-    return key;
-}
-
 CxHashKey cx_hash_key(
         const void *obj,
         size_t len
@@ -143,22 +93,6 @@
     return key;
 }
 
-CxHashKey cx_hash_key_u32(uint32_t x) {
-    CxHashKey key;
-    key.data = NULL;
-    key.len = 0;
-    key.hash = cx_hash_u32(x);
-    return key;
-}
-
-CxHashKey cx_hash_key_u64(uint64_t x) {
-    CxHashKey key;
-    key.data = NULL;
-    key.len = 0;
-    key.hash = cx_hash_u64(x);
-    return key;
-}
-
 int cx_hash_key_cmp(const void *l, const void *r) {
     const CxHashKey *left = l;
     const CxHashKey *right = r;
@@ -170,3 +104,7 @@
     if (left->len == 0) return 0;
     return memcmp(left->data, right->data, left->len);
 }
+
+cxstring cx_hash_key_as_string(const CxHashKey *key) {
+    return cx_strn(key->data, key->len);
+}
--- a/ucx/hash_map.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/hash_map.c	Wed Dec 31 16:40:12 2025 +0100
@@ -400,6 +400,13 @@
         cx_hash_map_iterator,
 };
 
+static int cx_map_cmpfunc2_safe_memcmp(const void *a, const void *b, void *c) {
+    // it is not safe to store a pointer to the size in the list
+    // because the entire list structure might get reallocated
+    size_t elem_size = (size_t)(uintptr_t)c;
+    return memcmp(a, b, elem_size);
+}
+
 CxMap *cxHashMapCreate(
         const CxAllocator *allocator,
         size_t itemsize,
@@ -432,8 +439,8 @@
 
     if (itemsize > 0) {
         map->base.collection.elem_size = itemsize;
-        map->base.collection.advanced_cmp = cx_acmp_memcmp;
-        map->base.collection.cmp_data = &map->base.collection.elem_size;
+        map->base.collection.advanced_cmp = cx_map_cmpfunc2_safe_memcmp;
+        map->base.collection.cmp_data = (void*)(uintptr_t)itemsize;
     } else {
         map->base.collection.elem_size = sizeof(void *);
         map->base.collection.store_pointer = true;
--- a/ucx/json.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/json.c	Wed Dec 31 16:40:12 2025 +0100
@@ -98,21 +98,21 @@
 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) {
+    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
+        str = cx_strcat(json->uncompleted_content, 1, str);
+        if (str.ptr == NULL) {
+            return (CxJsonToken){CX_JSON_NO_TOKEN, false, CX_NULLSTR}; // LCOV_EXCL_LINE
+        }
+        json->uncompleted_content = CX_NULLSTR;
+        json->uncompleted_tokentype = CX_JSON_NO_TOKEN;
     }
-    json->uncompleted = (CxJsonToken){0};
     CxJsonTokenType ttype;
     if (isstring) {
         ttype = CX_JSON_TOKEN_STRING;
     } else {
-        cxstring s = cx_strcast(str);
-        if (!cx_strcmp(s, "true") || !cx_strcmp(s, "false")
-            || !cx_strcmp(s, "null")) {
+        if (!cx_strcmp(str, "true") || !cx_strcmp(str, "false")
+            || !cx_strcmp(str, "null")) {
             ttype = CX_JSON_TOKEN_LITERAL;
         } else {
             ttype = token_numbertype(str.ptr, str.length);
@@ -122,7 +122,7 @@
         if (allocated) {
             cx_strfree(&str);
         }
-        return (CxJsonToken){CX_JSON_TOKEN_ERROR, false, {NULL, 0}};
+        return (CxJsonToken){CX_JSON_TOKEN_ERROR, false, CX_NULLSTR};
     }
     return (CxJsonToken){ttype, allocated, str};
 }
@@ -162,16 +162,16 @@
 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 ?
+        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;
+    CxJsonTokenType ttype = json->uncompleted_tokentype;
     size_t token_part_start = json->buffer.pos;
 
     bool escape_end_of_string = ttype == CX_JSON_TOKEN_STRING
-        && json->uncompleted.content.ptr[json->uncompleted.content.length-1] == '\\';
+        && cx_strat(json->uncompleted_content, -1) == '\\';
 
     for (size_t i = json->buffer.pos; i < json->buffer.size; i++) {
         char c = json->buffer.space[i];
@@ -189,7 +189,7 @@
                 } else if (ctype != CX_JSON_NO_TOKEN) {
                     // single-char token
                     json->buffer.pos = i + 1;
-                    *result = (CxJsonToken){ctype, false, {NULL, 0}};
+                    *result = (CxJsonToken){ctype, false, CX_NULLSTR};
                     return CX_JSON_NO_ERROR;
                 } else {
                     ttype = CX_JSON_TOKEN_LITERAL; // number or literal
@@ -232,31 +232,25 @@
         return CX_JSON_NO_DATA;
     } else {
         // uncompleted token
-        size_t uncompleted_len = json->buffer.size - token_part_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_part_start, uncompleted_len))
-            };
-            if (uncompleted.content.ptr == NULL) {
+        cxstring uncompleted = cx_strn(json->buffer.space + token_part_start, json->buffer.size - token_part_start);
+        if (json->uncompleted_tokentype == CX_JSON_NO_TOKEN) {
+            assert(json->uncompleted_content.ptr == NULL);
+            json->uncompleted_content = cx_strdup(uncompleted);
+            if (json->uncompleted_content.ptr == NULL) {
                 return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
-            json->uncompleted = uncompleted;
+            json->uncompleted_tokentype = ttype;
         } else {
-            // previously we also had an uncompleted token
+            // previously we already 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_part_start, uncompleted_len));
-            if (str.ptr == NULL) {
+            cxmutstr s = cx_strcat(json->uncompleted_content, 1, uncompleted);
+            if (s.ptr == NULL) {
                 return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
-            json->uncompleted.content = str;
+            json->uncompleted_content = s;
         }
         // advance the buffer position - we saved the stuff in the uncompleted token
-        json->buffer.pos += uncompleted_len;
+        json->buffer.pos += uncompleted.length;
         return CX_JSON_INCOMPLETE_DATA;
     }
 }
@@ -564,7 +558,8 @@
     }
     cxJsonValueFree(json->parsed);
     json->parsed = NULL;
-    token_destroy(&json->uncompleted);
+    json->uncompleted_tokentype = CX_JSON_NO_TOKEN;
+    cx_strfree(&json->uncompleted_content);
     cx_strfree_a(json->allocator, &json->uncompleted_member_name);
 }
 
@@ -1083,12 +1078,7 @@
         return NULL;
     }
     CxJsonValue *ret = value->array.data[index];
-    // TODO: replace with a low level cx_array_remove()
-    size_t count = value->array.size - index - 1;
-    if (count > 0) {
-        memmove(value->array.data + index, value->array.data + index + 1, count * sizeof(CxJsonValue*));
-    }
-    value->array.size--;
+    cx_array_remove(value->array, index);
     return ret;
 }
 
@@ -1231,7 +1221,7 @@
 
                 // the name
                 actual += wfunc("\"", 1, 1, target);
-                cxstring key = cx_strn(member->key->data, member->key->len);
+                cxstring key = cx_hash_key_as_string(member->key);
                 cxmutstr name = escape_string(key, settings->escape_slash);
                 actual += wfunc(name.ptr, 1, name.length, target);
                 actual += wfunc("\"", 1, 1, target);
@@ -1437,17 +1427,17 @@
     CxBuffer buffer;
     if (cxBufferInit(&buffer, allocator, NULL, 128,
                      CX_BUFFER_AUTO_EXTEND | CX_BUFFER_DO_NOT_FREE)) {
-        return (cxmutstr){NULL, 0};
+        return CX_NULLSTR; // LCOV_EXCL_LINE
     }
     if (cx_json_write_rec(&buffer, value, cxBufferWriteFunc, writer, 0)
             || cxBufferTerminate(&buffer)) {
         // LCOV_EXCL_START
         buffer.flags &= ~CX_BUFFER_DO_NOT_FREE;
         cxBufferDestroy(&buffer);
-        return (cxmutstr){NULL, 0};
+        return CX_NULLSTR;
         // LCOV_EXCL_STOP
     } else {
-        cxmutstr str = cx_mutstrn(buffer.space, buffer.size);
+        cxmutstr str = cx_bstr_m(&buffer);
         cxBufferDestroy(&buffer);
         return str;
     }
@@ -1495,8 +1485,7 @@
             return cx_vcmp_double(json->number, cxJsonAsDouble(other));
         case CX_JSON_LITERAL:
             return json->literal == other->literal ? 0 : -1;
-        default:
-            // LCOV_EXCL_START
+        default: // LCOV_EXCL_START
             // unreachable
             assert(false);
             return -1;
@@ -1509,7 +1498,7 @@
 }
 
 CxJsonValue* cx_json_clone_func(CxJsonValue* target, const CxJsonValue* source,
-        const CxAllocator* allocator, cx_attr_unused void *data) {
+        const CxAllocator* allocator, CX_UNUSED void *data) {
     if (source == NULL || source->type == CX_JSON_NOTHING) {
         return &cx_json_value_nothing;
     }
@@ -1521,7 +1510,8 @@
             return ret; \
         } else { \
             *target = *ret; \
-            cxFree(allocator, ret); \
+            ret->type = CX_JSON_UNINITIALIZED; \
+            cxJsonValueFree(ret); \
             return target; \
         } \
     }
@@ -1545,8 +1535,7 @@
             arr->array.size = elem_count;
             for (size_t i = 0 ; i < elem_count ; i++) {
                 CxJsonValue *e = cx_json_clone_func(NULL, source->array.data[i], allocator, NULL);
-                if (e == NULL) {
-                    // LCOV_EXCL_START
+                if (e == NULL) { // LCOV_EXCL_START
                     cxJsonValueFree(arr);
                     return NULL;
                     // LCOV_EXCL_STOP
@@ -1563,8 +1552,7 @@
             return_value(cxJsonCreateNumber(allocator, source->number));
         case CX_JSON_LITERAL:
             return_value(cxJsonCreateLiteral(allocator, source->literal));
-        default:
-            // LCOV_EXCL_START
+        default: // LCOV_EXCL_START
             // unreachable
             assert(false);
             return NULL;
--- a/ucx/kv_list.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/kv_list.c	Wed Dec 31 16:40:12 2025 +0100
@@ -515,7 +515,7 @@
 }
 
 static int cx_kvl_change_capacity(struct cx_list_s *list,
-        cx_attr_unused size_t cap) {
+        CX_UNUSED size_t cap) {
     // since our backing list is a linked list, we don't need to do much here,
     // but rehashing the map is quite useful
     cx_kv_list *kv_list = (cx_kv_list*)list;
--- a/ucx/linked_list.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/linked_list.c	Wed Dec 31 16:40:12 2025 +0100
@@ -105,7 +105,7 @@
 ) {
     cx_compare_func_wrapper wrapper = {cmp_func};
     return cx_linked_list_find_c(start, loc_advance, loc_data,
-        elem, found_index, cx_acmp_wrap, &wrapper);
+        elem, found_index, cx_cmp_wrap, &wrapper);
 }
 
 void *cx_linked_list_first(
@@ -254,19 +254,6 @@
     }
 }
 
-void cx_linked_list_insert_sorted(
-        void **begin,
-        void **end,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next,
-        void *new_node,
-        cx_compare_func cmp_func
-) {
-    assert(ll_next(new_node) == NULL);
-    cx_linked_list_insert_sorted_chain(
-            begin, end, loc_prev, loc_next, new_node, cmp_func);
-}
-
 static void *cx_linked_list_insert_sorted_chain_impl(
         void **begin,
         void **end,
@@ -409,6 +396,19 @@
     return dup_begin;
 }
 
+void cx_linked_list_insert_sorted(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *new_node,
+        cx_compare_func cmp_func
+) {
+    assert(ll_next(new_node) == NULL);
+    cx_linked_list_insert_sorted_chain(
+            begin, end, loc_prev, loc_next, new_node, cmp_func);
+}
+
 void cx_linked_list_insert_sorted_chain(
         void **begin,
         void **end,
@@ -420,7 +420,7 @@
     cx_compare_func_wrapper wrapper = {cmp_func};
     cx_linked_list_insert_sorted_chain_impl(
             begin, end, loc_prev, loc_next,
-            insert_begin, cx_acmp_wrap, &wrapper, true);
+            insert_begin, cx_cmp_wrap, &wrapper, true);
 }
 
 int cx_linked_list_insert_unique(
@@ -447,7 +447,63 @@
     cx_compare_func_wrapper wrapper = {cmp_func};
     return cx_linked_list_insert_sorted_chain_impl(
             begin, end, loc_prev, loc_next,
-            insert_begin, cx_acmp_wrap, &wrapper, false);
+            insert_begin, cx_cmp_wrap, &wrapper, false);
+}
+
+void cx_linked_list_insert_sorted_c(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *new_node,
+        cx_compare_func2 cmp_func,
+        void *context
+) {
+    assert(ll_next(new_node) == NULL);
+    cx_linked_list_insert_sorted_chain_c(
+            begin, end, loc_prev, loc_next, new_node, cmp_func, context);
+}
+
+void cx_linked_list_insert_sorted_chain_c(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *insert_begin,
+        cx_compare_func2 cmp_func,
+        void *context
+) {
+    cx_linked_list_insert_sorted_chain_impl(
+            begin, end, loc_prev, loc_next,
+            insert_begin, cmp_func, context, true);
+}
+
+int cx_linked_list_insert_unique_c(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *new_node,
+        cx_compare_func2 cmp_func,
+        void *context
+) {
+    assert(ll_next(new_node) == NULL);
+    return NULL != cx_linked_list_insert_unique_chain_c(
+            begin, end, loc_prev, loc_next, new_node, cmp_func, context);
+}
+
+void *cx_linked_list_insert_unique_chain_c(
+        void **begin,
+        void **end,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next,
+        void *insert_begin,
+        cx_compare_func2 cmp_func,
+        void *context
+) {
+    return cx_linked_list_insert_sorted_chain_impl(
+            begin, end, loc_prev, loc_next,
+            insert_begin, cmp_func, context, false);
 }
 
 size_t cx_linked_list_remove_chain(
@@ -661,7 +717,7 @@
         cx_compare_func cmp_func
 ) {
     cx_compare_func_wrapper wrapper = {cmp_func};
-    cx_linked_list_sort_c(begin, end, loc_prev, loc_next, loc_data, cx_acmp_wrap, &wrapper);
+    cx_linked_list_sort_c(begin, end, loc_prev, loc_next, loc_data, cx_cmp_wrap, &wrapper);
 }
 
 int cx_linked_list_compare_c(
@@ -697,7 +753,7 @@
 ) {
     cx_compare_func_wrapper wrapper = {cmp_func};
     return cx_linked_list_compare_c(begin_left, begin_right,
-            loc_advance, loc_data, cx_acmp_wrap, &wrapper);
+            loc_advance, loc_data, cx_cmp_wrap, &wrapper);
 }
 
 void cx_linked_list_reverse(
@@ -1328,8 +1384,8 @@
 void cx_linked_list_extra_data(cx_linked_list *list, size_t len) {
     list->extra_data_len = len;
 
-    off_t loc_extra = list->loc_data + list->base.collection.elem_size;
+    off_t loc_extra = list->loc_data + (off_t) list->base.collection.elem_size;
     size_t alignment = alignof(void*);
-    size_t padding = alignment - (loc_extra % alignment);
-    list->loc_extra = loc_extra + padding;
+    size_t padding = alignment - ((size_t)loc_extra % alignment);
+    list->loc_extra = loc_extra + (off_t) padding;
 }
--- a/ucx/list.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/list.c	Wed Dec 31 16:40:12 2025 +0100
@@ -31,6 +31,12 @@
 #include <string.h>
 #include <assert.h>
 
+// we don't want to include the full array_list.h.
+// therefore, we only forward declare the one function we want to use
+CX_EXPORT void cx_array_qsort_c(void *array, size_t nmemb, size_t size,
+        cx_compare_func2 fn, void *context);
+
+
 int cx_list_compare_wrapper(const void *l, const void *r, void *c) {
     CxList *list = c;
     const void *left;
@@ -57,33 +63,33 @@
 
 // <editor-fold desc="empty list implementation">
 
-static void cx_emptyl_noop(cx_attr_unused CxList *list) {
+static void cx_emptyl_noop(CX_UNUSED CxList *list) {
     // this is a noop, but MUST be implemented
 }
 
 static void *cx_emptyl_at(
-        cx_attr_unused const struct cx_list_s *list,
-        cx_attr_unused size_t index
+        CX_UNUSED const struct cx_list_s *list,
+        CX_UNUSED size_t index
 ) {
     return NULL;
 }
 
 static size_t cx_emptyl_find_remove(
-        cx_attr_unused struct cx_list_s *list,
-        cx_attr_unused const void *elem,
-        cx_attr_unused bool remove
+        CX_UNUSED struct cx_list_s *list,
+        CX_UNUSED const void *elem,
+        CX_UNUSED bool remove
 ) {
     return 0;
 }
 
-static bool cx_emptyl_iter_valid(cx_attr_unused const void *iter) {
+static bool cx_emptyl_iter_valid(CX_UNUSED const void *iter) {
     return false;
 }
 
 static CxIterator cx_emptyl_iterator(
         const struct cx_list_s *list,
         size_t index,
-        cx_attr_unused bool backwards
+        CX_UNUSED bool backwards
 ) {
     CxIterator iter = {0};
     iter.src_handle = (void*) list;
@@ -141,8 +147,7 @@
     const char *src = data;
     size_t i = 0;
     for (; i < n; i++) {
-        if (NULL == list->cl->insert_element(list, index + i, src)
-        ) {
+        if (NULL == list->cl->insert_element(list, index + i, src)) {
             return i; // LCOV_EXCL_LINE
         }
         if (src != NULL) {
@@ -283,12 +288,6 @@
     return cx_list_default_insert_sorted_impl(list, sorted_data, n, false);
 }
 
-// TODO: remove this hack once we have a solution for qsort() / qsort_s()
-static _Thread_local CxList *cx_hack_for_qsort_list;
-static int cx_hack_cmp_for_qsort(const void *l, const void *r) {
-    return cx_list_compare_wrapper(l, r, cx_hack_for_qsort_list);
-}
-
 void cx_list_default_sort(struct cx_list_s *list) {
     size_t elem_size = list->collection.elem_size;
     size_t list_size = list->collection.size;
@@ -304,9 +303,7 @@
     }
 
     // qsort
-    // TODO: qsort_s() is not as nice as we thought
-    cx_hack_for_qsort_list = list;
-    qsort(tmp, list_size, elem_size, cx_hack_cmp_for_qsort);
+    cx_array_qsort_c(tmp, list_size, elem_size, cx_list_compare_wrapper, list);
 
     // copy elements back
     loc = tmp;
@@ -341,6 +338,13 @@
     return 0;
 }
 
+static int cx_list_cmpfunc2_safe_memcmp(const void *a, const void *b, void *c) {
+    // it is not safe to store a pointer to the size in the list
+    // because the entire list structure might get reallocated
+    size_t elem_size = (size_t)(uintptr_t)c;
+    return memcmp(a, b, elem_size);
+}
+
 void cx_list_init(
     struct cx_list_s *list,
     struct cx_list_class_s *cl,
@@ -354,8 +358,8 @@
     if (elem_size > 0) {
         list->collection.elem_size = elem_size;
         list->collection.simple_cmp = NULL;
-        list->collection.advanced_cmp = cx_acmp_memcmp;
-        list->collection.cmp_data = &list->collection.elem_size;
+        list->collection.advanced_cmp = cx_list_cmpfunc2_safe_memcmp;
+        list->collection.cmp_data = (void*)(uintptr_t)list->collection.elem_size;
         list->collection.store_pointer = false;
     } else {
         list->collection.elem_size = sizeof(void *);
--- a/ucx/map.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/map.c	Wed Dec 31 16:40:12 2025 +0100
@@ -33,24 +33,24 @@
 
 // <editor-fold desc="empty map implementation">
 
-static void cx_empty_map_noop(cx_attr_unused CxMap *map) {
+static void cx_empty_map_noop(CX_UNUSED CxMap *map) {
     // this is a noop, but MUST be implemented
 }
 
 static void *cx_empty_map_get(
-        cx_attr_unused const CxMap *map,
-        cx_attr_unused CxHashKey key
+        CX_UNUSED const CxMap *map,
+        CX_UNUSED CxHashKey key
 ) {
     return NULL;
 }
 
-static bool cx_empty_map_iter_valid(cx_attr_unused const void *iter) {
+static bool cx_empty_map_iter_valid(CX_UNUSED const void *iter) {
     return false;
 }
 
 static CxMapIterator cx_empty_map_iterator(
         const struct cx_map_s *map,
-        cx_attr_unused enum cx_map_iterator_type type
+        CX_UNUSED enum cx_map_iterator_type type
 ) {
     CxMapIterator iter = {0};
     iter.map = (CxMap*) map;
--- a/ucx/properties.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/properties.c	Wed Dec 31 16:40:12 2025 +0100
@@ -112,7 +112,7 @@
         cxstring nl = cx_strchr(input, '\n');
         while (nl.length > 0) {
             // check for line continuation
-            char previous = nl.ptr > input.ptr ? nl.ptr[-1] : prop->buffer.space[prop->buffer.size-1];
+            char previous = nl.ptr > input.ptr ? nl.ptr[-1] : cx_strat(cx_bstr(&prop->buffer), -1);
             if (previous == continuation) {
                 // this nl is a line continuation, check the next newline
                 nl = cx_strchr(cx_strsubs(nl, 1), '\n');
@@ -128,7 +128,7 @@
 
             if (cxBufferAppend(input.ptr, 1,
                 len_until_nl, &prop->buffer) < len_until_nl) {
-                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+                return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
 
             // advance the position in the input buffer
@@ -197,7 +197,7 @@
                 cxBufferReset(&prop->buffer);
             }
             if (cxBufferAppend(buf, 1, len, &prop->buffer) < len) {
-                return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+                return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
             // reset the input buffer (make way for a re-fill)
             cxBufferReset(&prop->input);
@@ -256,7 +256,7 @@
                         prop->buffer.size = 0;
                         prop->buffer.pos = 0;
                         if (cxBufferWrite(val.ptr, 1, val.length, &prop->buffer) != val.length) {
-                            return CX_PROPERTIES_BUFFER_ALLOC_FAILED;
+                            return CX_PROPERTIES_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
                         }
                         val.ptr = prop->buffer.space;
                         ptr = prop->buffer.space;
@@ -338,12 +338,12 @@
     }
 
     // initialize the parser
-    char linebuf[cx_properties_load_buf_size];
-    char fillbuf[cx_properties_load_fill_size];
+    char linebuf[CX_PROPERTIES_LOAD_BUF_SIZE];
+    char fillbuf[CX_PROPERTIES_LOAD_FILL_SIZE];
     CxPropertiesStatus status;
     CxProperties parser;
     cxPropertiesInit(&parser, config);
-    cxPropertiesUseStack(&parser, linebuf, cx_properties_load_buf_size);
+    cxPropertiesUseStack(&parser, linebuf, CX_PROPERTIES_LOAD_BUF_SIZE);
 
     // read/fill/parse loop
     status = CX_PROPERTIES_NO_DATA;
@@ -351,15 +351,19 @@
     while (true) {
         size_t r = fread(fillbuf, 1, cx_properties_load_fill_size, f);
         if (ferror(f)) {
+            // LCOV_EXCL_START
             status = CX_PROPERTIES_FILE_ERROR;
             break;
+            // LCOV_EXCL_STOP
         }
         if (r == 0) {
             break;
         }
         if (cxPropertiesFilln(&parser, fillbuf, r)) {
+            // LCOV_EXCL_START
             status = CX_PROPERTIES_BUFFER_ALLOC_FAILED;
             break;
+            // LCOV_EXCL_STOP
         }
         cxstring key, value;
         while (true) {
@@ -368,15 +372,19 @@
                 break;
             } else {
                 cxmutstr v = cx_strdup_a(allocator, value);
+                // LCOV_EXCL_START
                 if (v.ptr == NULL) {
                     status = CX_PROPERTIES_MAP_ERROR;
                     break;
                 }
+                // LCOV_EXCL_STOP
                 void *mv = use_cstring ? (void*)v.ptr : &v;
                 if (cxMapPut(target, key, mv)) {
+                    // LCOV_EXCL_START
                     cx_strfree(&v);
                     status = CX_PROPERTIES_MAP_ERROR;
                     break;
+                    // LCOV_EXCL_STOP
                 }
                 keys_found++;
             }
--- a/ucx/streams.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/streams.c	Wed Dec 31 16:40:12 2025 +0100
@@ -29,12 +29,12 @@
 #include "cx/streams.h"
 #include "cx/allocator.h"
 
-#ifndef cx_strEAM_BCOPY_BUF_SIZE
-#define cx_strEAM_BCOPY_BUF_SIZE 8192
+#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
+#ifndef CX_STREAM_COPY_BUF_SIZE
+#define CX_STREAM_COPY_BUF_SIZE 1024
 #endif
 
 size_t cx_stream_bncopy(
@@ -57,7 +57,7 @@
         if (bufsize == 0) return 0;
         lbuf = buf;
     } else {
-        if (bufsize == 0) bufsize = cx_strEAM_BCOPY_BUF_SIZE;
+        if (bufsize == 0) bufsize = CX_STREAM_BCOPY_BUF_SIZE;
         lbuf = cxMallocDefault(bufsize);
         if (lbuf == NULL) return 0;
     }
@@ -88,7 +88,7 @@
         cx_write_func wfnc,
         size_t n
 ) {
-    char buf[cx_strEAM_COPY_BUF_SIZE];
+    char buf[CX_STREAM_COPY_BUF_SIZE];
     return cx_stream_bncopy(src, dest, rfnc, wfnc,
-                            buf, cx_strEAM_COPY_BUF_SIZE, n);
+                            buf, CX_STREAM_COPY_BUF_SIZE, n);
 }
--- a/ucx/string.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/string.c	Wed Dec 31 16:40:12 2025 +0100
@@ -26,8 +26,9 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-// for memrchr in glibc
+#ifdef WITH_MEMRCHR
 #define _GNU_SOURCE
+#endif
 
 #include "cx/string.h"
 
@@ -40,7 +41,12 @@
 #include <ctype.h>
 
 #ifdef _WIN32
-#define cx_strcasecmp_impl _strnicmp
+static int cx_fixed_strnicmp(const char* s1, const char* s2, size_t count) {
+    // Microsoft's implementation crashes when count == 0 and either string is NULL
+    if (count == 0) return 0;
+    return _strnicmp(s1, s2, count);
+}
+#define cx_strcasecmp_impl cx_fixed_strnicmp
 #else
 #include <strings.h>
 #define cx_strcasecmp_impl strncasecmp
@@ -63,7 +69,7 @@
     str->length = 0;
 }
 
-int cx_strcpy_a(
+int cx_strcpy_a_(
         const CxAllocator *alloc,
         cxmutstr *dest,
         cxstring src
@@ -98,13 +104,19 @@
     return size;
 }
 
-cxmutstr cx_strcat_ma(
+cxmutstr cx_strcat_a(
         const CxAllocator *alloc,
         cxmutstr str,
         size_t count,
         ...
 ) {
-    if (count == 0) return str;
+    if (count == 0) {
+        if (cxReallocate(alloc, &str.ptr, str.length + 1)) {
+            return CX_NULLSTR; // LCOV_EXCL_LINE
+        }
+        str.ptr[str.length] = '\0';
+        return str;
+    }
     va_list ap;
     va_start(ap, count);
     va_list ap2;
@@ -124,21 +136,16 @@
     if (overflow) {
         va_end(ap2);
         errno = EOVERFLOW;
-        return (cxmutstr) { NULL, 0 };
+        return CX_NULLSTR;
     }
 
-    // reallocate or create new string
-    char *newstr;
-    if (str.ptr == NULL) {
-        newstr = cxMalloc(alloc, slen + 1);
-    } else {
-        newstr = cxRealloc(alloc, str.ptr, slen + 1);
+    // reallocate or create a new string
+    if (cxReallocate(alloc, &str.ptr, slen + 1)) {
+        // LCOV_EXCL_START
+        va_end(ap2);
+        return CX_NULLSTR;
+        // LCOV_EXCL_STOP
     }
-    if (newstr == NULL) { // LCOV_EXCL_START
-        va_end(ap2);
-        return (cxmutstr) {NULL, 0};
-    } // LCOV_EXCL_STOP
-    str.ptr = newstr;
 
     // concatenate strings
     size_t pos = str.length;
@@ -156,21 +163,14 @@
     return str;
 }
 
-cxstring cx_strsubs(
+cxstring cx_strsubs_(
         cxstring string,
         size_t start
 ) {
-    return cx_strsubsl(string, start, string.length - start);
+    return cx_strsubsl_(string, start, string.length);
 }
 
-cxmutstr cx_strsubs_m(
-        cxmutstr string,
-        size_t start
-) {
-    return cx_strsubsl_m(string, start, string.length - start);
-}
-
-cxstring cx_strsubsl(
+cxstring cx_strsubsl_(
         cxstring string,
         size_t start,
         size_t length
@@ -187,16 +187,7 @@
     return (cxstring) {string.ptr + start, length};
 }
 
-cxmutstr cx_strsubsl_m(
-        cxmutstr string,
-        size_t start,
-        size_t length
-) {
-    cxstring result = cx_strsubsl(cx_strcast(string), start, length);
-    return (cxmutstr) {(char *) result.ptr, result.length};
-}
-
-cxstring cx_strchr(
+cxstring cx_strchr_(
         cxstring string,
         int chr
 ) {
@@ -205,15 +196,7 @@
     return (cxstring) {ret, string.length - (ret - string.ptr)};
 }
 
-cxmutstr cx_strchr_m(
-        cxmutstr string,
-        int chr
-) {
-    cxstring result = cx_strchr(cx_strcast(string), chr);
-    return (cxmutstr) {(char *) result.ptr, result.length};
-}
-
-cxstring cx_strrchr(
+cxstring cx_strrchr_(
     cxstring string,
     int chr
 ) {
@@ -234,23 +217,12 @@
 #endif
 }
 
-cxmutstr cx_strrchr_m(
-        cxmutstr string,
-        int chr
-) {
-    cxstring result = cx_strrchr(cx_strcast(string), chr);
-    return (cxmutstr) {(char *) result.ptr, result.length};
-}
+#ifndef CX_STRSTR_SBO_SIZE
+#define CX_STRSTR_SBO_SIZE 128
+#endif
+const unsigned cx_strstr_sbo_size = CX_STRSTR_SBO_SIZE;
 
-#ifndef cx_strSTR_SBO_SIZE
-#define cx_strSTR_SBO_SIZE 128
-#endif
-const unsigned cx_strstr_sbo_size = cx_strSTR_SBO_SIZE;
-
-cxstring cx_strstr(
-        cxstring haystack,
-        cxstring needle
-) {
+cxstring cx_strstr_(cxstring haystack, cxstring needle) {
     if (needle.length == 0) {
         return haystack;
     }
@@ -269,11 +241,11 @@
      */
 
     // local prefix table
-    size_t s_prefix_table[cx_strSTR_SBO_SIZE];
+    size_t s_prefix_table[CX_STRSTR_SBO_SIZE];
 
     // check needle length and use appropriate prefix table
     // if the pattern exceeds static prefix table, allocate on the heap
-    const bool useheap = needle.length >= cx_strSTR_SBO_SIZE;
+    const bool useheap = needle.length >= CX_STRSTR_SBO_SIZE;
     register size_t *ptable = useheap
         ? cxCallocDefault(needle.length + 1, sizeof(size_t))
         : s_prefix_table;
@@ -320,15 +292,7 @@
     return result;
 }
 
-cxmutstr cx_strstr_m(
-        cxmutstr haystack,
-        cxstring needle
-) {
-    cxstring result = cx_strstr(cx_strcast(haystack), needle);
-    return (cxmutstr) {(char *) result.ptr, result.length};
-}
-
-size_t cx_strsplit(
+size_t cx_strsplit_(
         cxstring string,
         cxstring delim,
         size_t limit,
@@ -386,7 +350,7 @@
     return n;
 }
 
-size_t cx_strsplit_a(
+size_t cx_strsplit_a_(
         const CxAllocator *allocator,
         cxstring string,
         cxstring delim,
@@ -415,27 +379,27 @@
         }
     }
     *output = cxCalloc(allocator, n, sizeof(cxstring));
-    return cx_strsplit(string, delim, n, *output);
+    return cx_strsplit_(string, delim, n, *output);
 }
 
-size_t cx_strsplit_m(
+size_t cx_strsplit_m_(
         cxmutstr string,
         cxstring delim,
         size_t limit,
         cxmutstr *output
 ) {
-    return cx_strsplit(cx_strcast(string),
+    return cx_strsplit_(cx_strcast(string),
                        delim, limit, (cxstring *) output);
 }
 
-size_t cx_strsplit_ma(
+size_t cx_strsplit_ma_(
         const CxAllocator *allocator,
         cxmutstr string,
         cxstring delim,
         size_t limit,
         cxmutstr **output
 ) {
-    return cx_strsplit_a(allocator, cx_strcast(string),
+    return cx_strsplit_a_(allocator, cx_strcast(string),
                          delim, limit, (cxstring **) output);
 }
 
@@ -510,23 +474,18 @@
     return result;
 }
 
-cxstring cx_strtrim(cxstring string) {
+cxstring cx_strtrim_(cxstring string) {
     cxstring result = string;
-    while (result.length > 0 && isspace((unsigned char)(result.ptr[0]))) {
+    while (isspace((unsigned char)cx_strat(result, 0))) {
         result.ptr++;
         result.length--;
     }
-    while (result.length > 0 && isspace((unsigned char)result.ptr[result.length - 1])) {
+    while (isspace((unsigned char)cx_strat(result, -1))) {
         result.length--;
     }
     return result;
 }
 
-cxmutstr cx_strtrim_m(cxmutstr string) {
-    cxstring result = cx_strtrim(cx_strcast(string));
-    return (cxmutstr) {(char *) result.ptr, result.length};
-}
-
 bool cx_strprefix_(
         cxstring string,
         cxstring prefix
@@ -570,7 +529,7 @@
 #endif
 }
 
-cxmutstr cx_strreplacen_a(
+cxmutstr cx_strreplace_(
         const CxAllocator *allocator,
         cxstring str,
         cxstring search,
@@ -651,7 +610,7 @@
     return ctx;
 }
 
-bool cx_strtok_next(
+bool cx_strtok_next_(
         CxStrtokCtx *ctx,
         cxstring *token
 ) {
@@ -694,13 +653,6 @@
     return true;
 }
 
-bool cx_strtok_next_m(
-        CxStrtokCtx *ctx,
-        cxmutstr *token
-) {
-    return cx_strtok_next(ctx, (cxstring *) token);
-}
-
 void cx_strtok_delim(
         CxStrtokCtx *ctx,
         const cxstring *delim,
--- a/ucx/tree.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ucx/tree.c	Wed Dec 31 16:40:12 2025 +0100
@@ -29,6 +29,7 @@
 #include "cx/tree.h"
 
 #include <assert.h>
+#include <string.h>
 
 #define CX_TREE_PTR(cur, off) (*(void**)(((char*)(cur))+(off)))
 #define tree_parent(node) CX_TREE_PTR(node, loc_parent)
@@ -37,36 +38,14 @@
 #define tree_prev(node) CX_TREE_PTR(node, loc_prev)
 #define tree_next(node) CX_TREE_PTR(node, loc_next)
 
-#define cx_tree_ptr_locations \
-    loc_parent, loc_children, loc_last_child, loc_prev, loc_next
-
-#define cx_tree_node_layout(tree) \
+#define tree_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,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-) {
-    tree_parent(node) = NULL;
-    if (loc_prev >= 0) {
-        tree_prev(node) = NULL;
-    }
-    tree_next(node) = NULL;
-    tree_children(node) = NULL;
-    if (loc_last_child >= 0) {
-        tree_last_child(node) = NULL;
-    }
-}
-
-void cx_tree_link(
+void cx_tree_add(
         void *parent,
         void *node,
         ptrdiff_t loc_parent,
@@ -82,7 +61,8 @@
     void *current_parent = tree_parent(node);
     if (current_parent == parent) return;
     if (current_parent != NULL) {
-        cx_tree_unlink(node, cx_tree_ptr_locations);
+        cx_tree_remove(node, loc_parent, loc_children,
+            loc_last_child, loc_prev, loc_next);
     }
 
     if (tree_children(parent) == NULL) {
@@ -128,7 +108,7 @@
     }
 }
 
-void cx_tree_unlink(
+void cx_tree_remove(
         void *node,
         ptrdiff_t loc_parent,
         ptrdiff_t loc_children,
@@ -175,21 +155,20 @@
     }
 }
 
-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 cx_tree_search(const void *root, size_t max_depth,
+        const void *data, cx_tree_search_func sfunc, void **result,
+        ptrdiff_t loc_children, ptrdiff_t loc_next) {
     // help avoiding bugs due to uninitialized memory
     assert(result != NULL);
     *result = NULL;
 
+    // NULL root? exit!
+    if (root == NULL) {
+        return -1;
+    }
+
     // remember return value for best match
-    int ret = sfunc(root, node);
+    int ret = sfunc(root, data);
     if (ret < 0) {
         // not contained, exit
         return -1;
@@ -201,13 +180,13 @@
     }
 
     // when depth is one, we are already done
-    if (depth == 1) {
+    if (max_depth == 1) {
         return ret;
     }
 
     // special case: indefinite depth
-    if (depth == 0) {
-        depth = SIZE_MAX;
+    if (max_depth == 0) {
+        max_depth = SIZE_MAX;
     }
 
     // create an iterator
@@ -221,7 +200,7 @@
     // loop through the remaining tree
     cx_foreach(void *, elem, iter) {
         // investigate the current node
-        int ret_elem = sfunc(elem, node);
+        int ret_elem = sfunc(elem, data);
         if (ret_elem == 0) {
             // if found, exit the search
             *result = elem;
@@ -237,47 +216,30 @@
         }
 
         // when we reached the max depth, skip the subtree
-        if (iter.depth == depth) {
+        if (iter.depth == max_depth) {
             cxTreeIteratorContinue(iter);
         }
     }
 
-    // dispose the iterator as we might have exited the loop early
+    // dispose of the iterator as we might have exited the loop early
     cxTreeIteratorDispose(&iter);
 
     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,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_next
-) {
-    // it is basically the same implementation
-    return cx_tree_search(
-            root, depth, data,
-            (cx_tree_search_func) sfunc,
-            result,
-            loc_children, loc_next);
-}
-
 static bool cx_tree_iter_valid(const void *it) {
-    const struct cx_tree_iterator_s *iter = it;
+    const CxTreeIterator *iter = it;
     return iter->node != NULL;
 }
 
 static void *cx_tree_iter_current(const void *it) {
-    const struct cx_tree_iterator_s *iter = it;
+    const CxTreeIterator *iter = it;
     return iter->node;
 }
 
 static void cx_tree_iter_next(void *it) {
-    struct cx_tree_iterator_s *iter = it;
+    CxTreeIterator *iter = it;
     ptrdiff_t const loc_next = iter->loc_next;
     ptrdiff_t const loc_children = iter->loc_children;
     // protect us from misuse
@@ -350,16 +312,16 @@
         }
     } else {
         // node has children, push the first child onto the stack and enter it
-        if (iter->stack_size >= iter->stack_capacity) {
+        if (iter->depth >= iter->stack_capacity) {
             const size_t newcap = iter->stack_capacity + 8;
-            if (cxReallocArrayDefault(&iter->stack, newcap, sizeof(void*))) {
+            if (cxReallocateArrayDefault(&iter->stack, newcap, sizeof(void*))) {
                 // we cannot return an error in this function
                 abort(); // LCOV_EXCL_LINE
             }
             iter->stack_capacity = newcap;
         }
-        iter->stack[iter->stack_size] = children;
-        iter->stack_size++;
+        iter->stack[iter->depth] = children;
+        iter->depth++;
         iter->node = children;
         iter->counter++;
     }
@@ -371,55 +333,56 @@
         ptrdiff_t loc_children,
         ptrdiff_t loc_next
 ) {
-    CxTreeIterator iter;
-    iter.loc_children = loc_children;
-    iter.loc_next = loc_next;
-    iter.visit_on_exit = visit_on_exit;
+    CxTreeIterator ret;
+    ret.use_dfs = true;
+    ret.loc_children = loc_children;
+    ret.loc_next = loc_next;
+    ret.visit_on_exit = visit_on_exit;
 
     // initialize members
-    iter.node_next = NULL;
-    iter.exiting = false;
-    iter.skip = false;
+    ret.node_next = NULL;
+    ret.exiting = false;
+    ret.skip = false;
 
     // assign base iterator functions
-    iter.base.allow_remove = false;
-    iter.base.remove = false;
-    iter.base.current_impl = NULL;
-    iter.base.valid = cx_tree_iter_valid;
-    iter.base.next = cx_tree_iter_next;
-    iter.base.current = cx_tree_iter_current;
+    ret.base.allow_remove = false;
+    ret.base.remove = false;
+    ret.base.current_impl = NULL;
+    ret.base.valid = cx_tree_iter_valid;
+    ret.base.next = cx_tree_iter_next;
+    ret.base.current = cx_tree_iter_current;
 
     // visit the root node
-    iter.node = root;
+    ret.node = root;
     if (root != NULL) {
-        iter.stack_capacity = 16;
-        iter.stack = cxMallocDefault(sizeof(void *) * 16);
-        iter.stack[0] = root;
-        iter.counter = 1;
-        iter.depth = 1;
+        ret.stack_capacity = 16;
+        ret.stack = cxMallocDefault(sizeof(void *) * 16);
+        ret.stack[0] = root;
+        ret.counter = 1;
+        ret.depth = 1;
     } else {
-        iter.stack_capacity = 0;
-        iter.stack = NULL;
-        iter.counter = 0;
-        iter.depth = 0;
+        ret.stack_capacity = 0;
+        ret.stack = NULL;
+        ret.counter = 0;
+        ret.depth = 0;
     }
 
-    return iter;
+    return ret;
 }
 
 static bool cx_tree_visitor_valid(const void *it) {
-    const struct cx_tree_visitor_s *iter = it;
+    const CxTreeIterator *iter = it;
     return iter->node != NULL;
 }
 
 static void *cx_tree_visitor_current(const void *it) {
-    const struct cx_tree_visitor_s *iter = it;
+    const CxTreeIterator *iter = it;
     return iter->node;
 }
 
-cx_attr_nonnull
+CX_NONNULL
 static void cx_tree_visitor_enqueue_siblings(
-        struct cx_tree_visitor_s *iter, void *node, ptrdiff_t loc_next) {
+        CxTreeIterator *iter, void *node, ptrdiff_t loc_next) {
     node = tree_next(node);
     while (node != NULL) {
         struct cx_tree_visitor_queue_s *q;
@@ -434,7 +397,7 @@
 }
 
 static void cx_tree_visitor_next(void *it) {
-    struct cx_tree_visitor_s *iter = it;
+    CxTreeIterator *iter = it;
     // protect us from misuse
     if (!iter->base.valid(iter)) return;
 
@@ -488,358 +451,98 @@
     iter->counter++;
 }
 
-CxTreeVisitor cx_tree_visitor(
+CxTreeIterator cx_tree_visitor(
         void *root,
         ptrdiff_t loc_children,
         ptrdiff_t loc_next
 ) {
-    CxTreeVisitor iter;
-    iter.loc_children = loc_children;
-    iter.loc_next = loc_next;
+    CxTreeIterator ret;
+    ret.visit_on_exit = false;
+    ret.exiting = false;
+    ret.use_dfs = false;
+    ret.loc_children = loc_children;
+    ret.loc_next = loc_next;
 
     // initialize members
-    iter.skip = false;
-    iter.queue_next = NULL;
-    iter.queue_last = NULL;
+    ret.skip = false;
+    ret.queue_next = NULL;
+    ret.queue_last = NULL;
 
     // assign base iterator functions
-    iter.base.allow_remove = false;
-    iter.base.remove = false;
-    iter.base.current_impl = NULL;
-    iter.base.valid = cx_tree_visitor_valid;
-    iter.base.next = cx_tree_visitor_next;
-    iter.base.current = cx_tree_visitor_current;
+    ret.base.allow_remove = false;
+    ret.base.remove = false;
+    ret.base.current_impl = NULL;
+    ret.base.valid = cx_tree_visitor_valid;
+    ret.base.next = cx_tree_visitor_next;
+    ret.base.current = cx_tree_visitor_current;
 
     // visit the root node
-    iter.node = root;
+    ret.node = root;
     if (root != NULL) {
-        iter.counter = 1;
-        iter.depth = 1;
+        ret.counter = 1;
+        ret.depth = 1;
     } else {
-        iter.counter = 0;
-        iter.depth = 0;
+        ret.counter = 0;
+        ret.depth = 0;
     }
 
-    return iter;
-}
-
-static void cx_tree_add_link_duplicate(
-        void *original, void *duplicate,
-        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev, ptrdiff_t loc_next
-) {
-    void *shared_parent = tree_parent(original);
-    if (shared_parent == NULL) {
-        cx_tree_link(original, duplicate, cx_tree_ptr_locations);
-    } else {
-        cx_tree_link(shared_parent, duplicate, cx_tree_ptr_locations);
-    }
-}
-
-static void cx_tree_add_link_new(
-        void *parent, void *node, cx_tree_search_func sfunc,
-        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev, ptrdiff_t loc_next
-) {
-    // check the current children one by one,
-    // if they could be children of the new node
-    void *child = tree_children(parent);
-    while (child != NULL) {
-        void *next = tree_next(child);
-
-        if (sfunc(node, child) > 0) {
-            // the sibling could be a child -> re-link
-            cx_tree_link(node, child, cx_tree_ptr_locations);
-        }
-
-        child = next;
-    }
-
-    // add new node as new child
-    cx_tree_link(parent, node, cx_tree_ptr_locations);
-}
-
-int cx_tree_add(
-        const void *src,
-        cx_tree_search_func sfunc,
-        cx_tree_node_create_func cfunc,
-        void *cdata,
-        void **cnode,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-) {
-    *cnode = cfunc(src, cdata);
-    if (*cnode == NULL) return 1;  // LCOV_EXCL_LINE
-    cx_tree_zero_pointers(*cnode, cx_tree_ptr_locations);
-
-    void *match = NULL;
-    int result = cx_tree_search(
-            root,
-            0,
-            *cnode,
-            sfunc,
-            &match,
-            loc_children,
-            loc_next
-    );
-
-    if (result < 0) {
-        // node does not fit into the tree - return non-zero value
-        return 1;
-    } else if (result == 0) {
-        // data already found in the tree, link duplicate
-        cx_tree_add_link_duplicate(match, *cnode, cx_tree_ptr_locations);
-    } else {
-        // closest match found, add new node
-        cx_tree_add_link_new(match, *cnode, sfunc, cx_tree_ptr_locations);
-    }
-
-    return 0;
+    return ret;
 }
 
-unsigned int cx_tree_add_look_around_depth = 3;
-
-size_t cx_tree_add_iter(
-        struct cx_iterator_base_s *iter,
-        size_t num,
-        cx_tree_search_func sfunc,
-        cx_tree_node_create_func cfunc,
-        void *cdata,
-        void **failed,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-) {
-    // erase the failed pointer
-    *failed = NULL;
-
-    // iter not valid? cancel...
-    if (!iter->valid(iter)) return 0;
-
-    size_t processed = 0;
-    void *current_node = root;
-    const void *elem;
-
-    for (void **eptr; processed < num &&
-         iter->valid(iter) && (eptr = iter->current(iter)) != NULL;
-         iter->next(iter)) {
-        elem = *eptr;
-
-        // create the new node
-        void *new_node = cfunc(elem, cdata);
-        if (new_node == NULL) return processed;  // LCOV_EXCL_LINE
-        cx_tree_zero_pointers(new_node, cx_tree_ptr_locations);
-
-        // start searching from current node
-        void *match;
-        int result;
-        unsigned int look_around_retries = cx_tree_add_look_around_depth;
-        cx_tree_add_look_around_retry:
-        result = cx_tree_search(
-                current_node,
-                0,
-                new_node,
-                sfunc,
-                &match,
-                loc_children,
-                loc_next
-        );
-
-        if (result < 0) {
-            // traverse upwards and try to find better parents
-            void *parent = tree_parent(current_node);
-            if (parent != NULL) {
-                if (look_around_retries > 0) {
-                    look_around_retries--;
-                    current_node = parent;
-                } else {
-                    // look around retries exhausted, start from the root
-                    current_node = root;
-                }
-                goto cx_tree_add_look_around_retry;
-            } else {
-                // no parents. so we failed
-                *failed = new_node;
-                return processed;
-            }
-        } else if (result == 0) {
-            // data already found in the tree, link duplicate
-            cx_tree_add_link_duplicate(match, new_node, cx_tree_ptr_locations);
-            // but stick with the original match, in case we needed a new root
-            current_node = match;
-        } else {
-            // closest match found, add new node as child
-            cx_tree_add_link_new(match, new_node, sfunc,
-                                 cx_tree_ptr_locations);
-            current_node = match;
-        }
-
-        processed++;
+size_t cx_tree_size(void *root, ptrdiff_t loc_children, ptrdiff_t loc_next) {
+    CxTreeIterator iter = cx_tree_iterator(root, false, loc_children, loc_next);
+    while (cxIteratorValid(iter)) {
+        cxIteratorNext(iter);
     }
-    return processed;
+    return iter.counter;
 }
 
-size_t cx_tree_add_array(
-        const void *src,
-        size_t num,
-        size_t elem_size,
-        cx_tree_search_func sfunc,
-        cx_tree_node_create_func cfunc,
-        void *cdata,
-        void **failed,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-) {
-    // erase failed pointer
-    *failed = NULL;
-
-    // super special case: zero elements
-    if (num == 0) {
-        return 0;
+size_t cx_tree_depth(void *root, ptrdiff_t loc_children, ptrdiff_t loc_next) {
+    CxTreeIterator iter = cx_tree_iterator(root, false, loc_children, loc_next);
+    size_t depth = 0;
+    while (cxIteratorValid(iter)) {
+        if (iter.depth > depth) {
+            depth = iter.depth;
+        }
+        cxIteratorNext(iter);
     }
-
-    // special case: one element does not need an iterator
-    if (num == 1) {
-        void *node;
-        if (0 == cx_tree_add(
-                src, sfunc, cfunc, cdata, &node, root,
-                loc_parent, loc_children, loc_last_child,
-                loc_prev, loc_next)) {
-            return 1;
-        } else {
-            *failed = node;
-            return 0;
-        }
-    }
-
-    // otherwise, create iterator and hand over to other function
-    CxIterator iter = cxIterator(src, elem_size, num);
-    return cx_tree_add_iter(cxIteratorRef(iter), num, sfunc,
-                            cfunc, cdata, failed, root,
-                            loc_parent, loc_children, loc_last_child,
-                            loc_prev, loc_next);
+    return depth;
 }
 
-static int cx_tree_default_insert_element(
-        CxTree *tree,
-        const void *data
-) {
-    void *node;
-    if (tree->root == NULL) {
-        node = tree->node_create(data, tree);
-        if (node == NULL) return 1;  // LCOV_EXCL_LINE
-        cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
-        tree->root = node;
-        tree->collection.size = 1;
-        return 0;
-    }
-    int result = cx_tree_add(data, tree->search, tree->node_create,
-                tree, &node, tree->root, cx_tree_node_layout(tree));
-    if (0 == result) {
-        tree->collection.size++;
-    } else {
-        cxFree(tree->collection.allocator, node);
-    }
-    return result;
-}
+CxTree *cxTreeCreate(const CxAllocator *allocator,
+        size_t node_size, size_t elem_size, void *root, ptrdiff_t loc_data,
+        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev, ptrdiff_t loc_next) {
 
-static size_t cx_tree_default_insert_many(
-        CxTree *tree,
-        CxIteratorBase *iter,
-        size_t n
-) {
-    size_t ins = 0;
-    if (!iter->valid(iter)) return 0;
-    if (tree->root == NULL) {
-        // use the first element from the iter to create the root node
-        void **eptr = iter->current(iter);
-        void *node = tree->node_create(*eptr, tree);
-        if (node == NULL) return 0;  // LCOV_EXCL_LINE
-        cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
-        tree->root = node;
-        ins = 1;
-        iter->next(iter);
-    }
-    void *failed;
-    ins += cx_tree_add_iter(iter, n, tree->search, tree->node_create,
-                                  tree, &failed, tree->root, cx_tree_node_layout(tree));
-    tree->collection.size += ins;
-    if (ins < n) {
-        cxFree(tree->collection.allocator, failed);
-    }
-    return ins;
-}
-
-static void *cx_tree_default_find(
-        CxTree *tree,
-        const void *subtree,
-        const void *data,
-        size_t depth
-) {
-    if (tree->root == NULL) return NULL;  // LCOV_EXCL_LINE
-
-    void *found;
-    if (0 == cx_tree_search_data(
-            subtree,
-            depth,
-            data,
-            tree->search_data,
-            &found,
-            tree->loc_children,
-            tree->loc_next
-    )) {
-        return found;
-    } else {
-        return NULL;
-    }
-}
-
-static cx_tree_class cx_tree_default_class = {
-        cx_tree_default_insert_element,
-        cx_tree_default_insert_many,
-        cx_tree_default_find
-};
-
-CxTree *cxTreeCreate(const CxAllocator *allocator,
-        cx_tree_node_create_func create_func,
-        cx_tree_search_func search_func,
-        cx_tree_search_data_func search_data_func,
-        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
-        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 = cxZalloc(allocator, sizeof(CxTree));
     if (tree == NULL) return NULL;  // LCOV_EXCL_LINE
+    tree->collection.allocator = allocator;
 
-    tree->cl = &cx_tree_default_class;
-    tree->collection.allocator = allocator;
-    tree->node_create = create_func;
-    tree->search = search_func;
-    tree->search_data = search_data_func;
-    tree->collection.advanced_destructor = (cx_destructor_func2) cxFree;
-    tree->collection.destructor_data = (void *) allocator;
+    if (elem_size == CX_STORE_POINTERS) {
+        tree->collection.store_pointer = true;
+        tree->collection.elem_size = sizeof(void*);
+    } else {
+        tree->collection.elem_size = elem_size;
+    }
+
+    tree->root = root;
+    tree->node_size = node_size;
     tree->loc_parent = loc_parent;
     tree->loc_children = loc_children;
     tree->loc_last_child = loc_last_child;
     tree->loc_prev = loc_prev;
     tree->loc_next = loc_next;
+    tree->loc_data = loc_data;
+
+    if (root == NULL) {
+        cxSetAdvancedDestructor(tree, cxFree, (void*)allocator);
+    } else {
+        tree->collection.size = cx_tree_size(root, loc_children, loc_next);
+    }
 
     return tree;
 }
@@ -852,97 +555,120 @@
     cxFree(tree->collection.allocator, tree);
 }
 
-CxTree *cxTreeCreateWrapped(const CxAllocator *allocator, void *root,
-        ptrdiff_t loc_parent, ptrdiff_t loc_children, ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev, ptrdiff_t loc_next) {
-    if (allocator == NULL) {
-        allocator = cxDefaultAllocator;
-    }
-    assert(root != NULL);
-
-    CxTree *tree = cxZalloc(allocator, sizeof(CxTree));
-    if (tree == NULL) return NULL;  // LCOV_EXCL_LINE
-
-    tree->cl = &cx_tree_default_class;
-    // set the allocator anyway, just in case...
-    tree->collection.allocator = allocator;
-    tree->loc_parent = loc_parent;
-    tree->loc_children = loc_children;
-    tree->loc_last_child = loc_last_child;
-    tree->loc_prev = loc_prev;
-    tree->loc_next = loc_next;
-    tree->root = root;
-    tree->collection.size = cxTreeSubtreeSize(tree, root);
-    return tree;
-}
-
 void cxTreeSetParent(CxTree *tree, void *parent, void *child) {
     size_t loc_parent = tree->loc_parent;
     if (tree_parent(child) == NULL) {
         tree->collection.size++;
     }
-    cx_tree_link(parent, child, cx_tree_node_layout(tree));
+    cx_tree_add(parent, child, tree_layout(tree));
 }
 
-void cxTreeAddChildNode(CxTree *tree, void *parent, void *child) {
-    cx_tree_link(parent, child, cx_tree_node_layout(tree));
+void cxTreeAddNode(CxTree *tree, void *parent, void *child) {
+    cx_tree_add(parent, child, tree_layout(tree));
     tree->collection.size++;
 }
 
-int cxTreeAddChild(CxTree *tree, void *parent, const void *data) {
-    void *node = tree->node_create(data, tree);
-    if (node == NULL) return 1; // LCOV_EXCL_LINE
-    cx_tree_zero_pointers(node, cx_tree_node_layout(tree));
-    cx_tree_link(parent, node, cx_tree_node_layout(tree));
+void *cxTreeCreateNode(CxTree *tree, void *parent) {
+    void *node = cxZalloc(tree->collection.allocator, tree->node_size);
+    if (node == NULL) return NULL; // LCOV_EXCL_LINE
+    cx_tree_add(parent, node, tree_layout(tree));
     tree->collection.size++;
-    return 0;
+    return node;
 }
 
-int cxTreeInsert(CxTree *tree, const void *data) {
-    return tree->cl->insert_element(tree, data);
+void *cxTreeAddData(CxTree *tree, void *parent, const void *data) {
+    if (tree->loc_data < 0) return NULL;
+
+    void *node = cxTreeCreateNode(tree, parent);
+    if (node == NULL) return NULL; // LCOV_EXCL_LINE
+
+    char *dst = node;
+    dst += tree->loc_data;
+    const void *src = cxCollectionStoresPointers(tree) ? (const void*)&data : data;
+    memcpy(dst, src, tree->collection.elem_size);
+
+    return node;
+}
+
+void *cxTreeCreateRoot(CxTree *tree) {
+    if (tree->root != NULL) {
+        return tree->root;
+    }
+
+    void *node = cxZalloc(tree->collection.allocator, tree->node_size);
+    if (node == NULL) return NULL; // LCOV_EXCL_LINE
+    tree->root = node;
+    tree->collection.size = 1;
+    return node;
 }
 
-size_t cxTreeInsertIter(CxTree *tree, CxIteratorBase *iter, size_t n) {
-    return tree->cl->insert_many(tree, iter, n);
+void *cxTreeCreateRootData(CxTree *tree, const void *data) {
+    if (tree->loc_data < 0) return NULL;
+
+    void *node = cxTreeCreateRoot(tree);
+    if (node == NULL) return NULL; // LCOV_EXCL_LINE
+
+    char *dst = node;
+    dst += tree->loc_data;
+    const void *src = cxCollectionStoresPointers(tree) ? (const void*)&data : data;
+    memcpy(dst, src, tree->collection.elem_size);
+
+    return node;
+}
+
+void *cxTreeSetRoot(CxTree *tree, void *new_root) {
+    void *old_root = tree->root;
+    tree->root = new_root;
+    return old_root;
 }
 
-size_t cxTreeInsertArray(CxTree *tree, const void *data, size_t elem_size, size_t n) {
-    if (n == 0) return 0;
-    if (n == 1) return 0 == cxTreeInsert(tree, data) ? 1 : 0;
-    CxIterator iter = cxIterator(data, elem_size, n);
-    return cxTreeInsertIter(tree, cxIteratorRef(iter), n);
+void *cxTreeFindInSubtree(CxTree *tree, const void *data,
+        void *subtree_root, size_t max_depth, bool use_dfs) {
+    if (tree->loc_data < 0 || subtree_root == NULL) {
+        return NULL;
+    }
+
+    CxTreeIterator iter = use_dfs
+        ? cx_tree_iterator(subtree_root, false, tree->loc_children, tree->loc_next)
+        : cx_tree_visitor(subtree_root, tree->loc_children, tree->loc_next);
+
+    cx_foreach(char*, node, iter) {
+        char *node_data = node + tree->loc_data;
+        if (cxCollectionStoresPointers(tree)) {
+            node_data = *(void**)node_data;
+        }
+        if (cx_invoke_compare_func(tree, node_data, data) == 0) {
+            cxTreeIteratorDispose(&iter);
+            return node;
+        }
+        if (iter.depth == max_depth) {
+            cxTreeIteratorContinue(iter);
+        }
+    }
+    return NULL;
 }
 
-void *cxTreeFind( CxTree *tree, const void *data) {
-    return tree->cl->find(tree, tree->root, data, 0);
-}
-
-void *cxTreeFindInSubtree(CxTree *tree, const void *data, void *subtree_root, size_t max_depth) {
-    return tree->cl->find(tree, subtree_root, data, max_depth);
+void *cxTreeFindFastInSubtree(CxTree *tree, const void *data,
+        cx_tree_search_func sfunc, void *root, size_t max_depth) {
+    void *result;
+    int ret = cx_tree_search(root, max_depth, data, sfunc, &result,
+        tree->loc_children, tree->loc_next);
+    if (ret == 0) {
+        return result;
+    } else {
+        return NULL;
+    }
 }
 
 size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root) {
-    CxTreeVisitor visitor = cx_tree_visitor(
-            subtree_root,
-            tree->loc_children,
-            tree->loc_next
-    );
-    while (cxIteratorValid(visitor)) {
-        cxIteratorNext(visitor);
+    if (subtree_root == tree->root) {
+        return tree->collection.size;
     }
-    return visitor.counter;
+    return cx_tree_size(subtree_root, tree->loc_children, tree->loc_next);
 }
 
 size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root) {
-    CxTreeVisitor visitor = cx_tree_visitor(
-            subtree_root,
-            tree->loc_children,
-            tree->loc_next
-    );
-    while (cxIteratorValid(visitor)) {
-        cxIteratorNext(visitor);
-    }
-    return visitor.depth;
+    return cx_tree_depth(subtree_root, tree->loc_children, tree->loc_next);
 }
 
 size_t cxTreeSize(CxTree *tree) {
@@ -950,13 +676,7 @@
 }
 
 size_t cxTreeDepth(CxTree *tree) {
-    CxTreeVisitor visitor = cx_tree_visitor(
-            tree->root, tree->loc_children, tree->loc_next
-    );
-    while (cxIteratorValid(visitor)) {
-        cxIteratorNext(visitor);
-    }
-    return visitor.depth;
+    return cx_tree_depth(tree->root, tree->loc_children, tree->loc_next);
 }
 
 int cxTreeRemoveNode(
@@ -971,7 +691,7 @@
     void *new_parent = tree_parent(node);
 
     // first, unlink from the parent
-    cx_tree_unlink(node, cx_tree_node_layout(tree));
+    cx_tree_remove(node, tree_layout(tree));
 
     // then relink each child
     ptrdiff_t loc_children = tree->loc_children;
@@ -988,7 +708,7 @@
         }
 
         // link to new parent
-        cx_tree_link(new_parent, child, cx_tree_node_layout(tree));
+        cx_tree_add(new_parent, child, tree_layout(tree));
 
         // proceed to next child
         child = tree_next(child);
@@ -1012,7 +732,7 @@
         return;
     }
     size_t subtree_size = cxTreeSubtreeSize(tree, node);
-    cx_tree_unlink(node, cx_tree_node_layout(tree));
+    cx_tree_remove(node, tree_layout(tree));
     tree->collection.size -= subtree_size;
 }
 
@@ -1023,7 +743,7 @@
 ) {
     int result = cxTreeRemoveNode(tree, node, relink_func);
     if (result == 0) {
-        cx_invoke_destructor(tree, node);
+        cx_invoke_destructor_raw(tree, node);
         return 0;
     } else {
         return result;
@@ -1031,14 +751,15 @@
 }
 
 void cxTreeDestroySubtree(CxTree *tree, void *node) {
-    cx_tree_unlink(node, cx_tree_node_layout(tree));
+    cx_tree_remove(node, tree_layout(tree));
     CxTreeIterator iter = cx_tree_iterator(
             node, true,
             tree->loc_children, tree->loc_next
     );
     cx_foreach(void *, child, iter) {
         if (iter.exiting) {
-            cx_invoke_destructor(tree, child);
+            // always call the destructors with the node!
+            cx_invoke_destructor_raw(tree, child);
         }
     }
     tree->collection.size -= iter.counter;
@@ -1048,18 +769,18 @@
 }
 
 void cxTreeIteratorDispose(CxTreeIterator *iter) {
-    cxFreeDefault(iter->stack);
-    iter->stack = NULL;
-}
-
-void cxTreeVisitorDispose(CxTreeVisitor *visitor) {
-    struct cx_tree_visitor_queue_s *q = visitor->queue_next;
-    while (q != NULL) {
-        struct cx_tree_visitor_queue_s *next = q->next;
-        cxFreeDefault(q);
-        q = next;
+    if (iter->use_dfs) {
+        cxFreeDefault(iter->stack);
+        iter->stack = NULL;
+    } else {
+        struct cx_tree_visitor_queue_s *q = iter->queue_next;
+        while (q != NULL) {
+            struct cx_tree_visitor_queue_s *next = q->next;
+            cxFreeDefault(q);
+            q = next;
+        }
+        iter->queue_next = iter->queue_last = NULL;
     }
-    visitor->queue_next = visitor->queue_last = NULL;
 }
 
 CxTreeIterator cxTreeIterateSubtree(CxTree *tree, void *node, bool visit_on_exit) {
@@ -1069,7 +790,7 @@
     );
 }
 
-CxTreeVisitor cxTreeVisitSubtree(CxTree *tree, void *node) {
+CxTreeIterator cxTreeVisitSubtree(CxTree *tree, void *node) {
     return cx_tree_visitor(
             node, tree->loc_children, tree->loc_next
     );
@@ -1079,6 +800,6 @@
     return cxTreeIterateSubtree(tree, tree->root, visit_on_exit);
 }
 
-CxTreeVisitor cxTreeVisit(CxTree *tree) {
+CxTreeIterator cxTreeVisit(CxTree *tree) {
     return cxTreeVisitSubtree(tree, tree->root);
 }
--- a/ui/motif/pathbar.c	Wed Dec 31 12:37:09 2025 +0100
+++ b/ui/motif/pathbar.c	Wed Dec 31 16:40:12 2025 +0100
@@ -186,9 +186,9 @@
     
     cxmutstr url;
     if(add_separator) {
-        url = cx_strcat(3, base, cx_str("/"), path);
+        url = cx_strcat(CX_NULLSTR, 3, base, cx_str("/"), path);
     } else {
-        url = cx_strcat(2, base, path);
+        url = cx_strcat(CX_NULLSTR, 2, base, path);
     }
     
     return url;

mercurial