update ucx

Tue, 25 Feb 2025 21:12:11 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Tue, 25 Feb 2025 21:12:11 +0100
changeset 16
04c9f8d8f03b
parent 15
862ab606ee06
child 17
23e678e6c917

update ucx

ucx/allocator.c file | annotate | diff | comparison | revisions
ucx/array_list.c file | annotate | diff | comparison | revisions
ucx/buffer.c file | annotate | diff | comparison | revisions
ucx/cx/allocator.h file | annotate | diff | comparison | revisions
ucx/cx/array_list.h file | annotate | diff | comparison | revisions
ucx/cx/buffer.h file | annotate | diff | comparison | revisions
ucx/cx/collection.h file | annotate | diff | comparison | revisions
ucx/cx/common.h file | annotate | diff | comparison | revisions
ucx/cx/compare.h file | annotate | diff | comparison | revisions
ucx/cx/hash_key.h file | annotate | diff | comparison | revisions
ucx/cx/hash_map.h file | annotate | diff | comparison | revisions
ucx/cx/iterator.h file | annotate | diff | comparison | revisions
ucx/cx/json.h file | annotate | diff | comparison | revisions
ucx/cx/linked_list.h file | annotate | diff | comparison | revisions
ucx/cx/list.h file | annotate | diff | comparison | revisions
ucx/cx/map.h file | annotate | diff | comparison | revisions
ucx/cx/mempool.h file | annotate | diff | comparison | revisions
ucx/cx/printf.h file | annotate | diff | comparison | revisions
ucx/cx/properties.h file | annotate | diff | comparison | revisions
ucx/cx/streams.h file | annotate | diff | comparison | revisions
ucx/cx/string.h file | annotate | diff | comparison | revisions
ucx/cx/test.h file | annotate | diff | comparison | revisions
ucx/cx/tree.h file | annotate | diff | comparison | revisions
ucx/hash_map.c file | annotate | diff | comparison | revisions
ucx/json.c file | annotate | diff | comparison | revisions
ucx/linked_list.c file | annotate | diff | comparison | revisions
ucx/list.c file | annotate | diff | comparison | revisions
ucx/map.c file | annotate | diff | comparison | revisions
ucx/properties.c file | annotate | diff | comparison | revisions
ucx/string.c file | annotate | diff | comparison | revisions
ucx/tree.c file | annotate | diff | comparison | revisions
--- a/ucx/allocator.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/allocator.c	Tue Feb 25 21:12:11 2025 +0100
@@ -30,7 +30,6 @@
 
 #include <errno.h>
 
-__attribute__((__malloc__, __alloc_size__(2)))
 static void *cx_malloc_stdlib(
         cx_attr_unused void *d,
         size_t n
@@ -38,7 +37,6 @@
     return malloc(n);
 }
 
-__attribute__((__warn_unused_result__, __alloc_size__(3)))
 static void *cx_realloc_stdlib(
         cx_attr_unused void *d,
         void *mem,
@@ -47,16 +45,14 @@
     return realloc(mem, n);
 }
 
-__attribute__((__malloc__, __alloc_size__(2, 3)))
 static void *cx_calloc_stdlib(
         cx_attr_unused void *d,
-        size_t nelem,
-        size_t n
+        size_t nmemb,
+        size_t size
 ) {
-    return calloc(nelem, n);
+    return calloc(nmemb, size);
 }
 
-__attribute__((__nonnull__))
 static void cx_free_stdlib(
         cx_attr_unused void *d,
         void *mem
@@ -75,10 +71,9 @@
         &cx_default_allocator_class,
         NULL
 };
-CxAllocator *cxDefaultAllocator = &cx_default_allocator;
+const CxAllocator * const cxDefaultAllocator = &cx_default_allocator;
 
-#undef cx_reallocate
-int cx_reallocate(
+int cx_reallocate_(
         void **mem,
         size_t n
 ) {
@@ -91,8 +86,7 @@
     }
 }
 
-#undef cx_reallocatearray
-int cx_reallocatearray(
+int cx_reallocatearray_(
         void **mem,
         size_t nmemb,
         size_t size
@@ -144,8 +138,7 @@
     }
 }
 
-#undef cxReallocate
-int cxReallocate(
+int cxReallocate_(
         const CxAllocator *allocator,
         void **mem,
         size_t n
@@ -159,8 +152,7 @@
     }
 }
 
-#undef cxReallocateArray
-int cxReallocateArray(
+int cxReallocateArray_(
         const CxAllocator *allocator,
         void **mem,
         size_t nmemb,
@@ -177,10 +169,10 @@
 
 void *cxCalloc(
         const CxAllocator *allocator,
-        size_t nelem,
-        size_t n
+        size_t nmemb,
+        size_t size
 ) {
-    return allocator->cl->calloc(allocator->data, nelem, n);
+    return allocator->cl->calloc(allocator->data, nmemb, size);
 }
 
 void cxFree(
--- a/ucx/array_list.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/array_list.c	Tue Feb 25 21:12:11 2025 +0100
@@ -126,27 +126,31 @@
     assert(array != NULL);
     assert(size != NULL);
     assert(capacity != NULL);
-    assert(reallocator != NULL);
+
+    // default reallocator
+    if (reallocator == NULL) {
+        reallocator = cx_array_default_reallocator;
+    }
 
     // determine size and capacity
     size_t oldcap;
     size_t oldsize;
     size_t max_size;
-    if (width == 0 || width == 8*sizeof(size_t)) {
+    if (width == 0 || width == sizeof(size_t)) {
         oldcap = *(size_t*) capacity;
         oldsize = *(size_t*) size;
         max_size = SIZE_MAX;
-    } else if (width == 16) {
+    } else if (width == sizeof(uint16_t)) {
         oldcap = *(uint16_t*) capacity;
         oldsize = *(uint16_t*) size;
         max_size = UINT16_MAX;
-    } else if (width == 8) {
+    } else if (width == sizeof(uint8_t)) {
         oldcap = *(uint8_t*) capacity;
         oldsize = *(uint8_t*) size;
         max_size = UINT8_MAX;
     }
 #if CX_WORDSIZE == 64
-    else if (width == 32) {
+    else if (width == sizeof(uint32_t)) {
         oldcap = *(uint32_t*) capacity;
         oldsize = *(uint32_t*) size;
         max_size = UINT32_MAX;
@@ -186,15 +190,15 @@
         *array = newmem;
 
         // store new capacity
-        if (width == 0 || width == 8*sizeof(size_t)) {
+        if (width == 0 || width == sizeof(size_t)) {
             *(size_t*) capacity = newcap;
-        } else if (width == 16) {
+        } else if (width == sizeof(uint16_t)) {
             *(uint16_t*) capacity = (uint16_t) newcap;
-        } else if (width == 8) {
+        } else if (width == sizeof(uint8_t)) {
             *(uint8_t*) capacity = (uint8_t) newcap;
         }
 #if CX_WORDSIZE == 64
-        else if (width == 32) {
+        else if (width == sizeof(uint32_t)) {
             *(uint32_t*) capacity = (uint32_t) newcap;
         }
 #endif
@@ -219,27 +223,31 @@
     assert(size != NULL);
     assert(capacity != NULL);
     assert(src != NULL);
-    assert(reallocator != NULL);
+
+    // default reallocator
+    if (reallocator == NULL) {
+        reallocator = cx_array_default_reallocator;
+    }
 
     // determine size and capacity
     size_t oldcap;
     size_t oldsize;
     size_t max_size;
-    if (width == 0 || width == 8*sizeof(size_t)) {
+    if (width == 0 || width == sizeof(size_t)) {
         oldcap = *(size_t*) capacity;
         oldsize = *(size_t*) size;
         max_size = SIZE_MAX;
-    } else if (width == 16) {
+    } else if (width == sizeof(uint16_t)) {
         oldcap = *(uint16_t*) capacity;
         oldsize = *(uint16_t*) size;
         max_size = UINT16_MAX;
-    } else if (width == 8) {
+    } else if (width == sizeof(uint8_t)) {
         oldcap = *(uint8_t*) capacity;
         oldsize = *(uint8_t*) size;
         max_size = UINT8_MAX;
     }
 #if CX_WORDSIZE == 64
-    else if (width == 32) {
+    else if (width == sizeof(uint32_t)) {
         oldcap = *(uint32_t*) capacity;
         oldsize = *(uint32_t*) size;
         max_size = UINT32_MAX;
@@ -303,18 +311,18 @@
 
     // if any of size or capacity changed, store them back
     if (newsize != oldsize || newcap != oldcap) {
-        if (width == 0 || width == 8*sizeof(size_t)) {
+        if (width == 0 || width == sizeof(size_t)) {
             *(size_t*) capacity = newcap;
             *(size_t*) size = newsize;
-        } else if (width == 16) {
+        } else if (width == sizeof(uint16_t)) {
             *(uint16_t*) capacity = (uint16_t) newcap;
             *(uint16_t*) size = (uint16_t) newsize;
-        } else if (width == 8) {
+        } else if (width == sizeof(uint8_t)) {
             *(uint8_t*) capacity = (uint8_t) newcap;
             *(uint8_t*) size = (uint8_t) newsize;
         }
 #if CX_WORDSIZE == 64
-        else if (width == 32) {
+        else if (width == sizeof(uint32_t)) {
             *(uint32_t*) capacity = (uint32_t) newcap;
             *(uint32_t*) size = (uint32_t) newsize;
         }
@@ -341,7 +349,11 @@
     assert(capacity != NULL);
     assert(cmp_func != NULL);
     assert(sorted_data != NULL);
-    assert(reallocator != NULL);
+
+    // default reallocator
+    if (reallocator == NULL) {
+        reallocator = cx_array_default_reallocator;
+    }
 
     // corner case
     if (elem_count == 0) return 0;
@@ -844,32 +856,42 @@
     }
 }
 
-static ssize_t cx_arl_find_remove(
+static size_t cx_arl_find_remove(
         struct cx_list_s *list,
         const void *elem,
         bool remove
 ) {
+    assert(list != NULL);
     assert(list->collection.cmpfunc != NULL);
-    assert(list->collection.size < SIZE_MAX / 2);
+    if (list->collection.size == 0) return 0;
     char *cur = ((const cx_array_list *) list)->data;
 
-    for (ssize_t i = 0; i < (ssize_t) list->collection.size; i++) {
+    // optimize with binary search, when sorted
+    if (list->collection.sorted) {
+        size_t i = cx_array_binary_search(
+            cur,
+            list->collection.size,
+            list->collection.elem_size,
+            elem,
+            list->collection.cmpfunc
+        );
+        if (remove && i < list->collection.size) {
+            cx_arl_remove(list, i, 1, NULL);
+        }
+        return i;
+    }
+
+    // fallback: linear search
+    for (size_t i = 0; i < list->collection.size; i++) {
         if (0 == list->collection.cmpfunc(elem, cur)) {
             if (remove) {
-                if (1 == cx_arl_remove(list, i, 1, NULL)) {
-                    return i;
-                } else {
-                    // should be unreachable
-                    return -1;  // LCOV_EXCL_LINE
-                }
-            } else {
-                return i;
+                cx_arl_remove(list, i, 1, NULL);
             }
+            return i;
         }
         cur += list->collection.elem_size;
     }
-
-    return -1;
+    return list->collection.size;
 }
 
 static void cx_arl_sort(struct cx_list_s *list) {
@@ -1001,22 +1023,13 @@
 
     cx_array_list *list = cxCalloc(allocator, 1, sizeof(cx_array_list));
     if (list == NULL) return NULL;
-
-    list->base.cl = &cx_array_list_class;
-    list->base.collection.allocator = allocator;
+    cx_list_init((CxList*)list, &cx_array_list_class,
+        allocator, comparator, elem_size);
     list->capacity = initial_capacity;
 
-    if (elem_size > 0) {
-        list->base.collection.elem_size = elem_size;
-        list->base.collection.cmpfunc = comparator;
-    } else {
-        elem_size = sizeof(void *);
-        list->base.collection.cmpfunc = comparator == NULL ? cx_cmp_ptr : comparator;
-        cxListStorePointers((CxList *) list);
-    }
-
     // allocate the array after the real elem_size is known
-    list->data = cxCalloc(allocator, initial_capacity, elem_size);
+    list->data = cxCalloc(allocator, initial_capacity,
+        list->base.collection.elem_size);
     if (list->data == NULL) { // LCOV_EXCL_START
         cxFree(allocator, list);
         return NULL;
--- a/ucx/buffer.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/buffer.c	Tue Feb 25 21:12:11 2025 +0100
@@ -71,12 +71,18 @@
     buffer->size = 0;
     buffer->pos = 0;
 
-    buffer->flush_func = NULL;
-    buffer->flush_target = NULL;
-    buffer->flush_blkmax = 0;
-    buffer->flush_blksize = 4096;
-    buffer->flush_threshold = SIZE_MAX;
+    buffer->flush = NULL;
+
+    return 0;
+}
 
+int cxBufferEnableFlushing(
+    CxBuffer *buffer,
+    CxBufferFlushConfig config
+) {
+    buffer->flush = malloc(sizeof(CxBufferFlushConfig));
+    if (buffer->flush == NULL) return -1; // LCOV_EXCL_LINE
+    memcpy(buffer->flush, &config, sizeof(CxBufferFlushConfig));
     return 0;
 }
 
@@ -84,6 +90,7 @@
     if (buffer->flags & CX_BUFFER_FREE_CONTENTS) {
         cxFree(buffer->allocator, buffer->bytes);
     }
+    free(buffer->flush);
     memset(buffer, 0, sizeof(CxBuffer));
 }
 
@@ -196,39 +203,52 @@
     }
 }
 
-/**
- * Helps flushing data to the flush target of a buffer.
- *
- * @param buffer the buffer containing the config
- * @param space the data to flush
- * @param size the element size
- * @param nitems the number of items
- * @return the number of items flushed
- */
-static size_t cx_buffer_write_flush_helper(
-        CxBuffer *buffer,
-        const unsigned char *space,
+static size_t cx_buffer_flush_helper(
+        const CxBuffer *buffer,
+        const unsigned char *src,
         size_t size,
         size_t nitems
 ) {
-    size_t pos = 0;
-    size_t remaining = nitems;
-    size_t max_items = buffer->flush_blksize / size;
-    while (remaining > 0) {
-        size_t items = remaining > max_items ? max_items : remaining;
-        size_t flushed = buffer->flush_func(
-                space + pos,
-                size, items,
-                buffer->flush_target);
+    // flush data from an arbitrary source
+    // does not need to be the buffer's contents
+    size_t max_items = buffer->flush->blksize / size;
+    size_t fblocks = 0;
+    size_t flushed_total = 0;
+    while (nitems > 0 && fblocks < buffer->flush->blkmax) {
+        fblocks++;
+        size_t items = nitems > max_items ? max_items : nitems;
+        size_t flushed = buffer->flush->wfunc(
+            src, size, items, buffer->flush->target);
         if (flushed > 0) {
-            pos += (flushed * size);
-            remaining -= flushed;
+            flushed_total += flushed;
+            src += flushed * size;
+            nitems -= flushed;
         } else {
             // if no bytes can be flushed out anymore, we give up
             break;
         }
     }
-    return nitems - remaining;
+    return flushed_total;
+}
+
+static size_t cx_buffer_flush_impl(CxBuffer *buffer, size_t size) {
+    // flush the current contents of the buffer
+    unsigned char *space = buffer->bytes;
+    size_t remaining = buffer->pos / size;
+    size_t flushed_total = cx_buffer_flush_helper(
+        buffer, space, size, remaining);
+
+    // shift the buffer left after flushing
+    // IMPORTANT: up to this point, copy on write must have been
+    // performed already, because we can't do error handling here
+    cxBufferShiftLeft(buffer, flushed_total*size);
+
+    return flushed_total;
+}
+
+size_t cxBufferFlush(CxBuffer *buffer) {
+    if (buffer_copy_on_write(buffer)) return 0;
+    return cx_buffer_flush_impl(buffer, 1);
 }
 
 size_t cxBufferWrite(
@@ -248,95 +268,98 @@
         return nitems;
     }
 
-    size_t len;
-    size_t nitems_out = nitems;
+    size_t len, total_flushed = 0;
+cx_buffer_write_retry:
     if (cx_szmul(size, nitems, &len)) {
         errno = EOVERFLOW;
-        return 0;
+        return total_flushed;
     }
-    size_t required = buffer->pos + len;
-    if (buffer->pos > required) {
-        return 0;
+    if (buffer->pos > SIZE_MAX - len) {
+        errno = EOVERFLOW;
+        return total_flushed;
     }
 
+    size_t required = buffer->pos + len;
     bool perform_flush = false;
     if (required > buffer->capacity) {
         if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
-            if (buffer->flush_blkmax > 0 && required > buffer->flush_threshold) {
+            if (buffer->flush != NULL && required > buffer->flush->threshold) {
                 perform_flush = true;
             } else {
                 if (cxBufferMinimumCapacity(buffer, required)) {
-                    return 0; // LCOV_EXCL_LINE
+                    return total_flushed; // LCOV_EXCL_LINE
                 }
             }
         } else {
-            if (buffer->flush_blkmax > 0) {
+            if (buffer->flush != NULL) {
                 perform_flush = true;
             } else {
-                // truncate data to be written, if we can neither extend nor flush
+                // truncate data, if we can neither extend nor flush
                 len = buffer->capacity - buffer->pos;
                 if (size > 1) {
                     len -= len % size;
                 }
-                nitems_out = len / size;
+                nitems = len / size;
             }
         }
     }
 
+    // check here and not above because of possible truncation
     if (len == 0) {
-        return len;
+        return total_flushed;
     }
 
-    if (perform_flush) {
-        size_t flush_max;
-        if (cx_szmul(buffer->flush_blkmax, buffer->flush_blksize, &flush_max)) {
-            errno = EOVERFLOW;
-            return 0;
-        }
-        size_t flush_pos = buffer->flush_func == NULL || buffer->flush_target == NULL
-                           ? buffer->pos
-                           : cx_buffer_write_flush_helper(buffer, buffer->bytes, 1, buffer->pos);
-        if (flush_pos == buffer->pos) {
-            // entire buffer has been flushed, we can reset
-            buffer->size = buffer->pos = 0;
-
-            size_t items_flush; // how many items can also be directly flushed
-            size_t items_keep; // how many items have to be written to the buffer
+    // check if we need to copy
+    if (buffer_copy_on_write(buffer)) return 0;
 
-            items_flush = flush_max >= required ? nitems : (flush_max - flush_pos) / size;
-            if (items_flush > 0) {
-                items_flush = cx_buffer_write_flush_helper(buffer, ptr, size, items_flush / size);
-                // in case we could not flush everything, keep the rest
+    // perform the operation
+    if (perform_flush) {
+        size_t items_flushed;
+        if (buffer->pos == 0) {
+            // if we don't have data in the buffer, but are instructed
+            // to flush, it means that we are supposed to relay the data
+            items_flushed = cx_buffer_flush_helper(buffer, ptr, size, nitems);
+            if (items_flushed == 0) {
+                // we needed to relay data, but could not flush anything
+                // i.e. we have to give up to avoid endless trying
+                return 0;
+            }
+            nitems -= items_flushed;
+            total_flushed += items_flushed;
+            if (nitems > 0) {
+                ptr = ((unsigned char*)ptr) + items_flushed * size;
+                goto cx_buffer_write_retry;
             }
-            items_keep = nitems - items_flush;
-            if (items_keep > 0) {
-                // try again with the remaining stuff
-                const unsigned char *new_ptr = ptr;
-                new_ptr += items_flush * size;
-                // report the directly flushed items as written plus the remaining stuff
-                return items_flush + cxBufferWrite(new_ptr, size, items_keep, buffer);
-            } else {
-                // all items have been flushed - report them as written
-                return nitems;
+            return total_flushed;
+        } else {
+            items_flushed = cx_buffer_flush_impl(buffer, size);
+            if (items_flushed == 0) {
+                // flush target is full, let's try to truncate
+                size_t remaining_space;
+                if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
+                    remaining_space = buffer->flush->threshold > buffer->pos
+                                          ? buffer->flush->threshold - buffer->pos
+                                          : 0;
+                } else {
+                    remaining_space = buffer->capacity > buffer->pos
+                                          ? buffer->capacity - buffer->pos
+                                          : 0;
+                }
+                nitems = remaining_space / size;
+                if (nitems == 0) {
+                    return total_flushed;
+                }
             }
-        } else if (flush_pos == 0) {
-            // nothing could be flushed at all, we immediately give up without writing any data
-            return 0;
-        } else {
-            // we were partially successful, we shift left and try again
-            cxBufferShiftLeft(buffer, flush_pos);
-            return cxBufferWrite(ptr, size, nitems, buffer);
+            goto cx_buffer_write_retry;
         }
     } else {
-        if (buffer_copy_on_write(buffer)) return 0;
         memcpy(buffer->bytes + buffer->pos, ptr, len);
         buffer->pos += len;
         if (buffer->pos > buffer->size) {
             buffer->size = buffer->pos;
         }
-        return nitems_out;
+        return total_flushed + nitems;
     }
-
 }
 
 size_t cxBufferAppend(
@@ -346,9 +369,19 @@
         CxBuffer *buffer
 ) {
     size_t pos = buffer->pos;
-    buffer->pos = buffer->size;
+    size_t append_pos = buffer->size;
+    buffer->pos = append_pos;    
     size_t written = cxBufferWrite(ptr, size, nitems, buffer);
-    buffer->pos = pos;
+    // the buffer might have been flushed
+    // we must compute a possible delta for the position
+    // expected: pos = append_pos + written
+    // -> if this is not the case, there is a delta
+    size_t delta = append_pos + written*size - buffer->pos;
+    if (delta > pos) {
+        buffer->pos = 0;
+    } else {
+        buffer->pos = pos - delta;
+    }
     return written;
 }
 
--- a/ucx/cx/allocator.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/allocator.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,7 +26,7 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file allocator.h
+ * @file allocator.h
  * Interface for custom allocators.
  */
 
@@ -46,8 +46,6 @@
     /**
      * The allocator's malloc() implementation.
      */
-    cx_attr_nonnull
-    cx_attr_nodiscard cx_attr_allocsize(2)
     void *(*malloc)(
             void *data,
             size_t n
@@ -56,8 +54,6 @@
     /**
      * The allocator's realloc() implementation.
      */
-    cx_attr_nodiscard
-    cx_attr_allocsize(3)
     void *(*realloc)(
             void *data,
             void *mem,
@@ -67,18 +63,15 @@
     /**
      * The allocator's calloc() implementation.
      */
-    cx_attr_nodiscard
-    cx_attr_allocsize(2, 3)
     void *(*calloc)(
             void *data,
-            size_t nelem,
-            size_t n
+            size_t nmemb,
+            size_t size
     );
 
     /**
      * The allocator's free() implementation.
      */
-    cx_attr_nonnull_arg(1)
     void (*free)(
             void *data,
             void *mem
@@ -107,13 +100,14 @@
 /**
  * A default allocator using standard library malloc() etc.
  */
-extern CxAllocator *cxDefaultAllocator;
+cx_attr_export
+extern const CxAllocator * const cxDefaultAllocator;
 
 /**
  * Function pointer type for destructor functions.
  *
  * A destructor function deallocates possible contents and MAY free the memory
- * pointed to by \p memory. Read the documentation of the respective function
+ * pointed to by @p memory. Read the documentation of the respective function
  * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in
  * that particular implementation.
  *
@@ -125,7 +119,7 @@
  * Function pointer type for destructor functions.
  *
  * A destructor function deallocates possible contents and MAY free the memory
- * pointed to by \p memory. Read the documentation of the respective function
+ * pointed to by @p memory. Read the documentation of the respective function
  * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in
  * that particular implementation.
  *
@@ -138,93 +132,103 @@
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place,
+ * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
  *
- * \par Error handling
- * \c errno will be set by realloc() on failure.
+ * @par Error handling
+ * @c errno will be set by realloc() on failure.
  *
  * @param mem pointer to the pointer to allocated block
  * @param n the new size in bytes
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_reallocatearray()
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-int cx_reallocate(
+cx_attr_export
+int cx_reallocate_(
         void **mem,
         size_t n
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place,
+ * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
  *
- * The size is calculated by multiplying \p nemb and \p size.
+ * The size is calculated by multiplying @p nemb and @p size.
  *
- * \par Error handling
- * \c errno will be set by realloc() on failure or when the multiplication of
- * \p nmemb and \p size overflows.
+ * @par Error handling
+ * @c errno will be set by realloc() on failure or when the multiplication of
+ * @p nmemb and @p size overflows.
  *
  * @param mem pointer to the pointer to allocated block
  * @param nmemb the number of elements
  * @param size the size of each element
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_reallocate()
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-int cx_reallocatearray(
+cx_attr_export
+int cx_reallocatearray_(
         void **mem,
         size_t nmemb,
         size_t size
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place,
+ * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
  *
- * \par Error handling
- * \c errno will be set by realloc() on failure.
+ * @par Error handling
+ * @c errno will be set by realloc() on failure.
  *
- * @param mem pointer to the pointer to allocated block
- * @param n the new size in bytes
- * @return zero on success, non-zero on failure
+ * @param mem (@c void**) pointer to the pointer to allocated block
+ * @param n (@c size_t) the new size in bytes
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_reallocatearray()
  */
-#define cx_reallocate(mem, n) cx_reallocate((void**)(mem), n)
+#define cx_reallocate(mem, n) cx_reallocate_((void**)(mem), n)
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place,
+ * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
  *
- * The size is calculated by multiplying \p nemb and \p size.
+ * The size is calculated by multiplying @p nemb and @p size.
+ *
+ * @par Error handling
+ * @c errno will be set by realloc() on failure or when the multiplication of
+ * @p nmemb and @p size overflows.
  *
- * \par Error handling
- * \c errno will be set by realloc() on failure or when the multiplication of
- * \p nmemb and \p size overflows.
- *
- * @param mem pointer to the pointer to allocated block
- * @param nmemb the number of elements
- * @param size the size of each element
- * @return zero on success, non-zero on failure
+ * @param mem (@c void**) pointer to the pointer to allocated block
+ * @param nmemb (@c size_t) the number of elements
+ * @param size (@c size_t) the size of each element
+ * @retval zero success
+ * @retval non-zero failure
  */
 #define cx_reallocatearray(mem, nmemb, size) \
-    cx_reallocatearray((void**)(mem), nmemb, size)
+    cx_reallocatearray_((void**)(mem), nmemb, size)
 
 /**
  * Free a block allocated by this allocator.
  *
- * \note Freeing a block of a different allocator is undefined.
+ * @note Freeing a block of a different allocator is undefined.
  *
  * @param allocator the allocator
  * @param mem a pointer to the block to free
  */
 cx_attr_nonnull_arg(1)
+cx_attr_export
 void cxFree(
         const CxAllocator *allocator,
         void *mem
 );
 
 /**
- * Allocate \p n bytes of memory.
+ * Allocate @p n bytes of memory.
  *
  * @param allocator the allocator
  * @param n the number of bytes
@@ -235,28 +239,30 @@
 cx_attr_malloc
 cx_attr_dealloc_ucx
 cx_attr_allocsize(2)
+cx_attr_export
 void *cxMalloc(
         const CxAllocator *allocator,
         size_t n
 );
 
 /**
- * Re-allocate the previously allocated block in \p mem, making the new block
- * \p n bytes long.
+ * Reallocate the previously allocated block in @p mem, making the new block
+ * @p n bytes long.
  * This function may return the same pointer that was passed to it, if moving
  * the memory was not necessary.
  *
- * \note Re-allocating a block allocated by a different allocator is undefined.
+ * @note Re-allocating a block allocated by a different allocator is undefined.
  *
  * @param allocator the allocator
  * @param mem pointer to the previously allocated block
  * @param n the new size in bytes
- * @return a pointer to the re-allocated memory
+ * @return a pointer to the reallocated memory
  */
 cx_attr_nodiscard
 cx_attr_nonnull_arg(1)
 cx_attr_dealloc_ucx
 cx_attr_allocsize(3)
+cx_attr_export
 void *cxRealloc(
         const CxAllocator *allocator,
         void *mem,
@@ -264,27 +270,28 @@
 );
 
 /**
- * Re-allocate the previously allocated block in \p mem, making the new block
- * \p n bytes long.
+ * Reallocate the previously allocated block in @p mem, making the new block
+ * @p n bytes long.
  * This function may return the same pointer that was passed to it, if moving
  * the memory was not necessary.
  *
- * The size is calculated by multiplying \p nemb and \p size.
- * If that multiplication overflows, this function returns \c NULL and \c errno
+ * The size is calculated by multiplying @p nemb and @p size.
+ * If that multiplication overflows, this function returns @c NULL and @c errno
  * will be set.
  *
- * \note Re-allocating a block allocated by a different allocator is undefined.
+ * @note Re-allocating a block allocated by a different allocator is undefined.
  *
  * @param allocator the allocator
  * @param mem pointer to the previously allocated block
  * @param nmemb the number of elements
  * @param size the size of each element
- * @return a pointer to the re-allocated memory
+ * @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_attr_export
 void *cxReallocArray(
         const CxAllocator *allocator,
         void *mem,
@@ -293,67 +300,72 @@
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place,
+ * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
- * This function acts like cxRealloc() using the pointer pointed to by \p mem.
+ * This function acts like cxRealloc() using the pointer pointed to by @p mem.
  *
- * \note Re-allocating a block allocated by a different allocator is undefined.
+ * @note Re-allocating a block allocated by a different allocator is undefined.
  *
- * \par Error handling
- * \c errno will be set, if the underlying realloc function does so.
+ * @par Error handling
+ * @c errno will be set, if the underlying realloc function does so.
  *
  * @param allocator the allocator
  * @param mem pointer to the pointer to allocated block
  * @param n the new size in bytes
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  */
 cx_attr_nodiscard
 cx_attr_nonnull
-int cxReallocate(
+cx_attr_export
+int cxReallocate_(
         const CxAllocator *allocator,
         void **mem,
         size_t n
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place,
+ * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
- * This function acts like cxRealloc() using the pointer pointed to by \p mem.
+ * This function acts like cxRealloc() using the pointer pointed to by @p mem.
  *
- * \note Re-allocating a block allocated by a different allocator is undefined.
+ * @note Re-allocating a block allocated by a different allocator is undefined.
  *
- * \par Error handling
- * \c errno will be set, if the underlying realloc function does so.
+ * @par Error handling
+ * @c errno will be set, if the underlying realloc function does so.
  *
- * @param allocator the allocator
- * @param mem pointer to the pointer to allocated block
- * @param n the new size in bytes
- * @return zero on success, non-zero on failure
+ * @param allocator (@c CxAllocator*) the allocator
+ * @param mem (@c void**) pointer to the pointer to allocated block
+ * @param n (@c size_t) the new size in bytes
+ * @retval zero success
+ * @retval non-zero failure
  */
 #define cxReallocate(allocator, mem, n) \
-    cxReallocate(allocator, (void**)(mem), n)
+    cxReallocate_(allocator, (void**)(mem), n)
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place,
+ * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
  * This function acts like cxReallocArray() using the pointer pointed to
- * by \p mem.
+ * by @p mem.
  *
- * \note Re-allocating a block allocated by a different allocator is undefined.
+ * @note Re-allocating a block allocated by a different allocator is undefined.
  *
- * \par Error handling
- * \c errno will be set, if the underlying realloc function does so or the
- * multiplication of \p nmemb and \p size overflows.
+ * @par Error handling
+ * @c errno will be set, if the underlying realloc function does so or the
+ * multiplication of @p nmemb and @p size overflows.
  *
  * @param allocator the allocator
  * @param mem pointer to the pointer to allocated block
  * @param nmemb the number of elements
  * @param size the size of each element
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero on failure
  */
 cx_attr_nodiscard
 cx_attr_nonnull
-int cxReallocateArray(
+cx_attr_export
+int cxReallocateArray_(
         const CxAllocator *allocator,
         void **mem,
         size_t nmemb,
@@ -361,32 +373,33 @@
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place,
+ * Reallocate a previously allocated block and changes the pointer in-place,
  * if necessary.
  * This function acts like cxReallocArray() using the pointer pointed to
- * by \p mem.
+ * by @p mem.
+ *
+ * @note Re-allocating a block allocated by a different allocator is undefined.
  *
- * \note Re-allocating a block allocated by a different allocator is undefined.
+ * @par Error handling
+ * @c errno will be set, if the underlying realloc function does so or the
+ * multiplication of @p nmemb and @p size overflows.
  *
- * \par Error handling
- * \c errno will be set, if the underlying realloc function does so or the
- * multiplication of \p nmemb and \p size overflows.
+ * @param allocator (@c CxAllocator*) the allocator
+ * @param mem (@c void**) pointer to the pointer to allocated block
+ * @param nmemb (@c size_t) the number of elements
+ * @param size (@c size_t) the size of each element
+ * @retval zero success
+ * @retval non-zero failure
+ */
+#define cxReallocateArray(allocator, mem, nmemb, size) \
+        cxReallocateArray_(allocator, (void**) (mem), nmemb, size)
+
+/**
+ * Allocate @p nmemb elements of @p n bytes each, all initialized to zero.
  *
  * @param allocator the allocator
- * @param mem pointer to the pointer to allocated block
  * @param nmemb the number of elements
- * @param size the size of each element
- * @return zero on success, non-zero on failure
- */
-#define cxReallocateArray(allocator, mem, nmemb, size) \
-        cxReallocateArray(allocator, (void**) (mem), nmemb, size)
-
-/**
- * Allocate \p nelem elements of \p n bytes each, all initialized to zero.
- *
- * @param allocator the allocator
- * @param nelem the number of elements
- * @param n the size of each element in bytes
+ * @param size the size of each element in bytes
  * @return a pointer to the allocated memory
  */
 cx_attr_nonnull_arg(1)
@@ -394,10 +407,11 @@
 cx_attr_malloc
 cx_attr_dealloc_ucx
 cx_attr_allocsize(2, 3)
+cx_attr_export
 void *cxCalloc(
         const CxAllocator *allocator,
-        size_t nelem,
-        size_t n
+        size_t nmemb,
+        size_t size
 );
 
 #ifdef __cplusplus
--- a/ucx/cx/array_list.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/array_list.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file array_list.h
- * \brief Array list implementation.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file array_list.h
+ * @brief Array list implementation.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 
@@ -47,49 +47,88 @@
  * The maximum item size in an array list that fits into stack buffer
  * when swapped.
  */
+cx_attr_export
 extern const unsigned cx_array_swap_sbo_size;
 
 /**
  * Declares variables for an array that can be used with the convenience macros.
  *
+ * @par Examples
+ * @code
+ * // integer array with at most 255 elements
+ * CX_ARRAY_DECLARE_SIZED(int, myarray, uint8_t)
+ *
+ * // array of MyObject* pointers where size and capacity are stored as unsigned int
+ * CX_ARRAY_DECLARE_SIZED(MyObject*, objects, unsigned int)
+ *
+ * // initializing code
+ * cx_array_initialize(myarray, 16); // reserve space for 16
+ * cx_array_initialize(objects, 100); // reserve space for 100
+ * @endcode
+ *
  * @param type the type of the data
  * @param name the name of the array
  * @param size_type the type of the size (should be uint8_t, uint16_t, uint32_t, or size_t)
  *
+ * @see cx_array_initialize()
  * @see cx_array_simple_add()
  * @see cx_array_simple_copy()
- * @see cx_array_initialize()
  * @see cx_array_simple_add_sorted()
  * @see cx_array_simple_insert_sorted()
  */
 #define CX_ARRAY_DECLARE_SIZED(type, name, size_type) \
-    type * name;                                      \
-    /** Array size. */ size_type name##_size;         \
+    type * name; \
+    /** Array size. */ size_type name##_size; \
     /** Array capacity. */ size_type name##_capacity
 
 /**
  * Declares variables for an array that can be used with the convenience macros.
  *
- * The size and capacity variables will have `size_t` type.
+ * The size and capacity variables will have @c size_t type.
  * Use #CX_ARRAY_DECLARE_SIZED() to specify a different type.
  *
+ * @par Examples
+ * @code
+ * // int array
+ * CX_ARRAY_DECLARE(int, myarray)
+ *
+ * // initializing code
+ * cx_array_initialize(myarray, 32); // reserve space for 32
+ * @endcode
+ *
  * @param type the type of the data
  * @param name the name of the array
  *
+ * @see cx_array_initialize()
  * @see cx_array_simple_add()
  * @see cx_array_simple_copy()
- * @see cx_array_initialize()
  * @see cx_array_simple_add_sorted()
  * @see cx_array_simple_insert_sorted()
  */
 #define CX_ARRAY_DECLARE(type, name) CX_ARRAY_DECLARE_SIZED(type, name, size_t)
 
 /**
- * Initializes an array declared with CX_ARRAY_DECLARE().
+ * Initializes an array with the given capacity.
+ *
+ * The type of the capacity depends on the type used during declaration.
+ *
+ * @par Examples
+ * @code
+ * CX_ARRAY_DECLARE_SIZED(int, arr1, uint8_t)
+ * CX_ARRAY_DECLARE(int, arr2) // size and capacity are implicitly size_t
+ *
+ * // initializing code
+ * cx_array_initialize(arr1, 500); // error: maximum for uint8_t is 255
+ * cx_array_initialize(arr2, 500); // OK
+ * @endcode
+ *
  *
  * The memory for the array is allocated with stdlib malloc().
- * @param array the array
+ * @param array the name of the array
  * @param capacity the initial capacity
+ * @see cx_array_initialize_a()
+ * @see CX_ARRAY_DECLARE_SIZED()
+ * @see CX_ARRAY_DECLARE()
  */
 #define cx_array_initialize(array, capacity) \
         array##_capacity = capacity; \
@@ -97,12 +136,26 @@
         array = malloc(sizeof(array[0]) * capacity)
 
 /**
- * Initializes an array declared with CX_ARRAY_DECLARE().
+ * Initializes an array with the given capacity using the specified allocator.
+ *
+ * @par Example
+ * @code
+ * CX_ARRAY_DECLARE(int, myarray)
+ *
  *
- * The memory for the array is allocated with the specified allocator.
- * @param allocator the allocator
- * @param array the array
+ * const CxAllocator *al = // ...
+ * cx_array_initialize_a(al, myarray, 128);
+ * // ...
+ * cxFree(al, myarray); // don't forget to free with same allocator
+ * @endcode
+ *
+ * The memory for the array is allocated with stdlib malloc().
+ * @param allocator (@c CxAllocator*) the allocator
+ * @param array the name of the array
  * @param capacity the initial capacity
+ * @see cx_array_initialize()
+ * @see CX_ARRAY_DECLARE_SIZED()
+ * @see CX_ARRAY_DECLARE()
  */
 #define cx_array_initialize_a(allocator, array, capacity) \
         array##_capacity = capacity; \
@@ -111,6 +164,8 @@
 
 /**
  * Defines a reallocation mechanism for arrays.
+ * You can create your own, use cx_array_reallocator(), or
+ * use the #cx_array_default_reallocator.
  */
 struct cx_array_reallocator_s {
     /**
@@ -126,7 +181,7 @@
      * @param capacity the new capacity (number of elements)
      * @param elem_size the size of each element
      * @param alloc a reference to this allocator
-     * @return a pointer to the reallocated memory or \c NULL on failure
+     * @return a pointer to the reallocated memory or @c NULL on failure
      */
     cx_attr_nodiscard
     cx_attr_nonnull_arg(4)
@@ -164,24 +219,29 @@
 /**
  * A default stdlib-based array reallocator.
  */
-extern struct cx_array_reallocator_s *cx_array_default_reallocator;
+cx_attr_export
+extern CxArrayReallocator *cx_array_default_reallocator;
 
 /**
  * Creates a new array reallocator.
  *
- * When \p allocator is \c NULL, the stdlib default allocator will be used.
+ * When @p allocator is @c NULL, the stdlib default allocator will be used.
  *
- * When \p stackmem is not \c NULL, the reallocator is supposed to be used
- * \em only for the specific array that is initially located at \p stackmem.
+ * When @p stackmem is not @c NULL, the reallocator is supposed to be used
+ * @em only for the specific array that is initially located at @p stackmem.
  * When reallocation is needed, the reallocator checks, if the array is
- * still located at \p stackmem and copies the contents to the heap.
+ * still located at @p stackmem and copies the contents to the heap.
+ *
+ * @note Invoking this function with both arguments @c NULL will return a
+ * reallocator that behaves like #cx_array_default_reallocator.
  *
  * @param allocator the allocator this reallocator shall be based on
  * @param stackmem the address of the array when the array is initially located
- * on the stack
+ * on the stack or shall not reallocated in place
  * @return an array reallocator
  */
-struct cx_array_reallocator_s cx_array_reallocator(
+cx_attr_export
+CxArrayReallocator cx_array_reallocator(
         const struct cx_allocator_s *allocator,
         const void *stackmem
 );
@@ -189,28 +249,35 @@
 /**
  * Reserves memory for additional elements.
  *
- * This function checks if the \p capacity of the array is sufficient to hold
- * at least \p size plus \p elem_count elements. If not, a reallocation is
- * performed with the specified \p reallocator.
+ * This function checks if the @p capacity of the array is sufficient to hold
+ * at least @p size plus @p elem_count elements. If not, a reallocation is
+ * performed with the specified @p reallocator.
+ * You can create your own reallocator by hand, use #cx_array_default_reallocator,
+ * or use the convenience function cx_array_reallocator() to create a custom reallocator.
  *
  * This function can be useful to replace subsequent calls to cx_array_copy()
  * with one single cx_array_reserve() and then - after guaranteeing a
  * sufficient capacity - use simple memmove() or memcpy().
  *
- * The \p width refers to the size and capacity. Both must have the same width.
- * Supported are 0, 8, 16, and 32, as well as 64 if running on a 64 bit
+ * The @p width in bytes refers to the size and capacity.
+ * Both must have the same width.
+ * Supported are 0, 1, 2, and 4, as well as 8 if running on a 64 bit
  * architecture. If set to zero, the native word width is used.
  *
  * @param array a pointer to the target array
  * @param size a pointer to the size of the array
  * @param capacity a pointer to the capacity of the array
- * @param width the width in bytes for the \p size and \p capacity or zero for default
+ * @param width the width in bytes for the @p size and @p capacity or zero for default
  * @param elem_size the size of one element
  * @param elem_count the number of expected additional elements
  * @param reallocator the array reallocator to use
- * @return zero on success, non-zero on failure
+ * (@c NULL defaults to #cx_array_default_reallocator)
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_array_reallocator()
  */
-cx_attr_nonnull
+cx_attr_nonnull_arg(1, 2, 3)
+cx_attr_export
 int cx_array_reserve(
         void **array,
         void *size,
@@ -218,36 +285,43 @@
         unsigned width,
         size_t elem_size,
         size_t elem_count,
-        struct cx_array_reallocator_s *reallocator
+        CxArrayReallocator *reallocator
 );
 
 /**
  * Copies elements from one array to another.
  *
- * The elements are copied to the \p target array at the specified \p index,
- * overwriting possible elements. The \p index does not need to be in range of
- * the current array \p size. If the new index plus the number of elements added
- * would extend the array's size, the remaining \p capacity is used.
+ * The elements are copied to the @p target array at the specified @p index,
+ * overwriting possible elements. The @p index does not need to be in range of
+ * the current array @p size. If the new index plus the number of elements added
+ * would extend the array's size, the remaining @p capacity is used.
  *
- * If the \p capacity is also insufficient to hold the new data, a reallocation
- * attempt is made with the specified \p reallocator.
+ * If the @p capacity is also insufficient to hold the new data, a reallocation
+ * attempt is made with the specified @p reallocator.
+ * You can create your own reallocator by hand, use #cx_array_default_reallocator,
+ * or use the convenience function cx_array_reallocator() to create a custom reallocator.
  *
- * The \p width refers to the size and capacity. Both must have the same width.
- * Supported are 0, 8, 16, and 32, as well as 64 if running on a 64 bit
+ * The @p width in bytes refers to the size and capacity.
+ * Both must have the same width.
+ * Supported are 0, 1, 2, and 4, as well as 8 if running on a 64 bit
  * architecture. If set to zero, the native word width is used.
  *
  * @param target a pointer to the target array
  * @param size a pointer to the size of the target array
  * @param capacity a pointer to the capacity of the target array
- * @param width the width in bytes for the \p size and \p capacity or zero for default
+ * @param width the width in bytes for the @p size and @p capacity or zero for default
  * @param index the index where the copied elements shall be placed
  * @param src the source array
  * @param elem_size the size of one element
  * @param elem_count the number of elements to copy
  * @param reallocator the array reallocator to use
- * @return zero on success, non-zero on failure
+ * (@c NULL defaults to #cx_array_default_reallocator)
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cx_array_reallocator()
  */
-cx_attr_nonnull
+cx_attr_nonnull_arg(1, 2, 3, 6)
+cx_attr_export
 int cx_array_copy(
         void **target,
         void *size,
@@ -257,103 +331,111 @@
         const void *src,
         size_t elem_size,
         size_t elem_count,
-        struct cx_array_reallocator_s *reallocator
+        CxArrayReallocator *reallocator
 );
 
 /**
  * Convenience macro that uses cx_array_copy() with a default layout and
  * the specified reallocator.
  *
- * @param reallocator the array reallocator to use
- * @param array the name of the array (NOT a pointer to the array)
- * @param index the index where the copied elements shall be placed
- * @param src the source array
- * @param count the number of elements to copy
- * @return zero on success, non-zero on failure
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param index (@c size_t) the index where the copied elements shall be placed
+ * @param src (@c void*) the source array
+ * @param count (@c size_t) the number of elements to copy
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_copy()
  */
 #define cx_array_simple_copy_a(reallocator, array, index, src, count) \
     cx_array_copy((void**)&(array), &(array##_size), &(array##_capacity), \
-        8*sizeof(array##_size), index, src, sizeof((array)[0]), count, \
+        sizeof(array##_size), index, src, sizeof((array)[0]), count, \
         reallocator)
 
 /**
  * Convenience macro that uses cx_array_copy() with a default layout and
  * the default reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
- * @param index the index where the copied elements shall be placed
- * @param src the source array
- * @param count the number of elements to copy
- * @return zero on success, non-zero on failure
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param index (@c size_t) the index where the copied elements shall be placed
+ * @param src (@c void*) the source array
+ * @param count (@c size_t) the number of elements to copy
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_copy_a()
  */
 #define cx_array_simple_copy(array, index, src, count) \
-    cx_array_simple_copy_a(cx_array_default_reallocator, \
-    array, index, src, count)
+    cx_array_simple_copy_a(NULL, array, index, src, count)
 
 /**
  * Convenience macro that uses cx_array_reserve() with a default layout and
  * the specified reallocator.
  *
- * @param reallocator the array reallocator to use
- * @param array the name of the array (NOT a pointer to the array)
- * @param count the number of expected additional elements
- * @return zero on success, non-zero on failure
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param count (@c size_t) the number of expected @em additional elements
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_reserve()
  */
 #define cx_array_simple_reserve_a(reallocator, array, count) \
     cx_array_reserve((void**)&(array), &(array##_size), &(array##_capacity), \
-        8*sizeof(array##_size), sizeof((array)[0]), count, \
+        sizeof(array##_size), sizeof((array)[0]), count, \
         reallocator)
 
 /**
  * Convenience macro that uses cx_array_reserve() with a default layout and
  * the default reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
- * @param count the number of expected additional elements
- * @return zero on success, non-zero on failure
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param count (@c size_t) the number of expected additional elements
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_reserve_a()
  */
 #define cx_array_simple_reserve(array, count) \
-    cx_array_simple_reserve_a(cx_array_default_reallocator, \
-    array, count)
+    cx_array_simple_reserve_a(NULL, array, count)
 
 /**
  * Adds an element to an array with the possibility of allocating more space.
  *
- * The element \p elem is added to the end of the \p target array which contains
- * \p size elements, already. The \p capacity must point to a variable denoting
+ * The element @p elem is added to the end of the @p target array which contains
+ * @p size elements, already. The @p capacity must point to a variable denoting
  * the current maximum number of elements the array can hold.
  *
  * If the capacity is insufficient to hold the new element, an attempt to
- * increase the \p capacity is made and the new capacity is written back.
+ * increase the @p capacity is made and the new capacity is written back.
+ *
+ * The \@ SIZE_TYPE is flexible and can be any unsigned integer type.
+ * It is important, however, that @p size and @p capacity are pointers to
+ * variables of the same type.
  *
- * @param target a pointer to the target array
- * @param size a pointer to the size of the target array
- * @param capacity a pointer to the capacity of the target array
- * @param elem_size the size of one element
- * @param elem a pointer to the element to add
- * @param reallocator the array reallocator to use
- * @return zero on success, non-zero on failure
+ * @param target (@c void**) a pointer to the target array
+ * @param size (@c SIZE_TYPE*) a pointer to the size of the target array
+ * @param capacity (@c SIZE_TYPE*) a pointer to the capacity of the target array
+ * @param elem_size (@c size_t) the size of one element
+ * @param elem (@c void*) a pointer to the element to add
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @retval zero success
+ * @retval non-zero failure
  */
 #define cx_array_add(target, size, capacity, elem_size, elem, reallocator) \
-    cx_array_copy((void**)(target), size, capacity, 8*sizeof(*(size)), \
+    cx_array_copy((void**)(target), size, capacity, sizeof(*(size)), \
     *(size), elem, elem_size, 1, reallocator)
 
 /**
  * Convenience macro that uses cx_array_add() with a default layout and
  * the specified reallocator.
  *
- * @param reallocator the array reallocator to use
- * @param array the name of the array (NOT a pointer to the array)
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
  * @param elem the element to add (NOT a pointer, address is automatically taken)
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_add()
  */
@@ -364,9 +446,10 @@
  * Convenience macro that uses cx_array_add() with a default layout and
  * the default reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
+ * @param array the name of the array (NOT a pointer or alias to the array)
  * @param elem the element to add (NOT a pointer, address is automatically taken)
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_add_a()
  */
@@ -377,10 +460,12 @@
  * Inserts a sorted array into another sorted array.
  *
  * If either the target or the source array is not already sorted with respect
- * to the specified \p cmp_func, the behavior is undefined.
+ * to the specified @p cmp_func, the behavior is undefined.
  *
  * If the capacity is insufficient to hold the new data, a reallocation
  * attempt is made.
+ * You can create your own reallocator by hand, use #cx_array_default_reallocator,
+ * or use the convenience function cx_array_reallocator() to create a custom reallocator.
  *
  * @param target a pointer to the target array
  * @param size a pointer to the size of the target array
@@ -390,9 +475,12 @@
  * @param elem_size the size of one element
  * @param elem_count the number of elements to insert
  * @param reallocator the array reallocator to use
- * @return zero on success, non-zero on failure
+ * (@c NULL defaults to #cx_array_default_reallocator)
+ * @retval zero success
+ * @retval non-zero failure
  */
-cx_attr_nonnull
+cx_attr_nonnull_arg(1, 2, 3, 5)
+cx_attr_export
 int cx_array_insert_sorted(
         void **target,
         size_t *size,
@@ -401,25 +489,31 @@
         const void *src,
         size_t elem_size,
         size_t elem_count,
-        struct cx_array_reallocator_s *reallocator
+        CxArrayReallocator *reallocator
 );
 
 /**
  * Inserts an element into a sorted array.
  *
  * If the target array is not already sorted with respect
- * to the specified \p cmp_func, the behavior is undefined.
+ * to the specified @p cmp_func, the behavior is undefined.
  *
  * If the capacity is insufficient to hold the new data, a reallocation
  * attempt is made.
  *
- * @param target a pointer to the target array
- * @param size a pointer to the size of the target array
- * @param capacity a pointer to the capacity of the target array
- * @param elem_size the size of one element
- * @param elem a pointer to the element to add
- * @param reallocator the array reallocator to use
- * @return zero on success, non-zero on failure
+ * The \@ SIZE_TYPE is flexible and can be any unsigned integer type.
+ * It is important, however, that @p size and @p capacity are pointers to
+ * variables of the same type.
+ *
+ * @param target (@c void**) a pointer to the target array
+ * @param size (@c SIZE_TYPE*) a pointer to the size of the target array
+ * @param capacity (@c SIZE_TYPE*) a pointer to the capacity of the target array
+ * @param elem_size (@c size_t) the size of one element
+ * @param elem (@c void*) a pointer to the element to add
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @retval zero success
+ * @retval non-zero failure
  */
 #define cx_array_add_sorted(target, size, capacity, elem_size, elem, cmp_func, reallocator) \
     cx_array_insert_sorted((void**)(target), size, capacity, cmp_func, elem, elem_size, 1, reallocator)
@@ -428,11 +522,12 @@
  * Convenience macro for cx_array_add_sorted() with a default
  * layout and the specified reallocator.
  *
- * @param reallocator the array reallocator to use
- * @param array the name of the array (NOT a pointer to the array)
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
  * @param elem the element to add (NOT a pointer, address is automatically taken)
- * @param cmp_func the compare function for the elements
- * @return zero on success, non-zero on failure
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_add_sorted()
  */
@@ -444,26 +539,28 @@
  * Convenience macro for cx_array_add_sorted() with a default
  * layout and the default reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
+ * @param array the name of the array (NOT a pointer or alias to the array)
  * @param elem the element to add (NOT a pointer, address is automatically taken)
- * @param cmp_func the compare function for the elements
- * @return zero on success, non-zero on failure
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_add_sorted_a()
  */
 #define cx_array_simple_add_sorted(array, elem, cmp_func) \
-    cx_array_simple_add_sorted_a(cx_array_default_reallocator, array, elem, cmp_func)
+    cx_array_simple_add_sorted_a(NULL, array, elem, cmp_func)
 
 /**
  * Convenience macro for cx_array_insert_sorted() with a default
  * layout and the specified reallocator.
  *
- * @param reallocator the array reallocator to use
- * @param array the name of the array (NOT a pointer to the array)
- * @param src pointer to the source array
- * @param n number of elements in the source array
- * @param cmp_func the compare function for the elements
- * @return zero on success, non-zero on failure
+ * @param reallocator (@c CxArrayReallocator*) the array reallocator to use
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param src (@c void*) pointer to the source array
+ * @param n (@c size_t) number of elements in the source array
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_insert_sorted()
  */
@@ -475,28 +572,29 @@
  * Convenience macro for cx_array_insert_sorted() with a default
  * layout and the default reallocator.
  *
- * @param array the name of the array (NOT a pointer to the array)
- * @param src pointer to the source array
- * @param n number of elements in the source array
- * @param cmp_func the compare function for the elements
- * @return zero on success, non-zero on failure
+ * @param array the name of the array (NOT a pointer or alias to the array)
+ * @param src (@c void*) pointer to the source array
+ * @param n (@c size_t) number of elements in the source array
+ * @param cmp_func (@c cx_cmp_func) the compare function for the elements
+ * @retval zero success
+ * @retval non-zero failure
  * @see CX_ARRAY_DECLARE()
  * @see cx_array_simple_insert_sorted_a()
  */
 #define cx_array_simple_insert_sorted(array, src, n, cmp_func) \
-    cx_array_simple_insert_sorted_a(cx_array_default_reallocator, array, src, n, cmp_func)
+    cx_array_simple_insert_sorted_a(NULL, array, src, n, cmp_func)
 
 /**
  * Searches the largest lower bound in a sorted array.
  *
  * In other words, this function returns the index of the largest element
- * in \p arr that is less or equal to \p elem with respect to \p cmp_func.
- * When no such element exists, \p size is returned.
+ * in @p arr that is less or equal to @p elem with respect to @p cmp_func.
+ * When no such element exists, @p size is returned.
  *
- * If \p elem is contained in the array, this is identical to
+ * If @p elem is contained in the array, this is identical to
  * #cx_array_binary_search().
  *
- * If the array is not sorted with respect to the \p cmp_func, the behavior
+ * If the array is not sorted with respect to the @p cmp_func, the behavior
  * is undefined.
  *
  * @param arr the array to search
@@ -504,9 +602,12 @@
  * @param elem_size the size of one element
  * @param elem the element to find
  * @param cmp_func the compare function
- * @return the index of the largest lower bound, or \p size
+ * @return the index of the largest lower bound, or @p size
+ * @see cx_array_binary_search_sup()
+ * @see cx_array_binary_search()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cx_array_binary_search_inf(
         const void *arr,
         size_t size,
@@ -518,7 +619,7 @@
 /**
  * Searches an item in a sorted array.
  *
- * If the array is not sorted with respect to the \p cmp_func, the behavior
+ * If the array is not sorted with respect to the @p cmp_func, the behavior
  * is undefined.
  *
  * @param arr the array to search
@@ -526,10 +627,13 @@
  * @param elem_size the size of one element
  * @param elem the element to find
  * @param cmp_func the compare function
- * @return the index of the element in the array, or \p size if the element
+ * @return the index of the element in the array, or @p size if the element
  * cannot be found
+ * @see cx_array_binary_search_inf()
+ * @see cx_array_binary_search_sup()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cx_array_binary_search(
         const void *arr,
         size_t size,
@@ -542,13 +646,13 @@
  * Searches the smallest upper bound in a sorted array.
  *
  * In other words, this function returns the index of the smallest element
- * in \p arr that is greater or equal to \p elem with respect to \p cmp_func.
- * When no such element exists, \p size is returned.
+ * in @p arr that is greater or equal to @p elem with respect to @p cmp_func.
+ * When no such element exists, @p size is returned.
  *
- * If \p elem is contained in the array, this is identical to
+ * If @p elem is contained in the array, this is identical to
  * #cx_array_binary_search().
  *
- * If the array is not sorted with respect to the \p cmp_func, the behavior
+ * If the array is not sorted with respect to the @p cmp_func, the behavior
  * is undefined.
  *
  * @param arr the array to search
@@ -556,9 +660,12 @@
  * @param elem_size the size of one element
  * @param elem the element to find
  * @param cmp_func the compare function
- * @return the index of the smallest upper bound, or \p size
+ * @return the index of the smallest upper bound, or @p size
+ * @see cx_array_binary_search_inf()
+ * @see cx_array_binary_search()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cx_array_binary_search_sup(
         const void *arr,
         size_t size,
@@ -576,6 +683,7 @@
  * @param idx2 index of second element
  */
 cx_attr_nonnull
+cx_attr_export
 void cx_array_swap(
         void *arr,
         size_t elem_size,
@@ -584,16 +692,16 @@
 );
 
 /**
- * Allocates an array list for storing elements with \p elem_size bytes each.
+ * Allocates an array list for storing elements with @p elem_size bytes each.
  *
- * If \p elem_size is CX_STORE_POINTERS, the created list will be created as if
- * cxListStorePointers() was called immediately after creation and the compare
- * function will be automatically set to cx_cmp_ptr(), if none is given.
+ * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
+ * copies of the added elements and the compare function will be automatically set
+ * to cx_cmp_ptr(), if none is given.
  *
  * @param allocator the allocator for allocating the list memory
- * (if \c NULL, a default stdlib allocator will be used)
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param comparator the comparator for the elements
- * (if \c NULL, and the list is not storing pointers, sort and find
+ * (if @c NULL, and the list is not storing pointers, sort and find
  * functions will not work)
  * @param elem_size the size of each element in bytes
  * @param initial_capacity the initial number of elements the array can store
@@ -602,6 +710,7 @@
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxListFree, 1)
+cx_attr_export
 CxList *cxArrayListCreate(
         const CxAllocator *allocator,
         cx_compare_func comparator,
@@ -610,18 +719,18 @@
 );
 
 /**
- * Allocates an array list for storing elements with \p elem_size bytes each.
+ * Allocates an array list for storing elements with @p elem_size bytes each.
  *
- * The list will use the cxDefaultAllocator and \em NO compare function.
+ * The list will use the cxDefaultAllocator and @em NO compare function.
  * If you want to call functions that need a compare function, you have to
  * set it immediately after creation or use cxArrayListCreate().
  *
- * If \p elem_size is CX_STORE_POINTERS, the created list will be created as if
- * cxListStorePointers() was called immediately after creation and the compare
- * function will be automatically set to cx_cmp_ptr().
+ * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
+ * copies of the added elements and the compare function will be automatically set
+ * to cx_cmp_ptr(), if none is given.
  *
- * @param elem_size the size of each element in bytes
- * @param initial_capacity the initial number of elements the array can store
+ * @param elem_size (@c size_t) the size of each element in bytes
+ * @param initial_capacity (@c size_t) the initial number of elements the array can store
  * @return the created list
  */
 #define cxArrayListCreateSimple(elem_size, initial_capacity) \
--- a/ucx/cx/buffer.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/buffer.h	Tue Feb 25 21:12:11 2025 +0100
@@ -27,9 +27,9 @@
  */
 
 /**
- * \file buffer.h
+ * @file buffer.h
  *
- * \brief Advanced buffer implementation.
+ * @brief Advanced buffer implementation.
  *
  * Instances of CxBuffer can be used to read from or to write to like one
  * would do with a stream.
@@ -38,9 +38,9 @@
  * can be enabled. See the documentation of the macro constants for more
  * information.
  *
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_BUFFER_H
@@ -88,8 +88,73 @@
  */
 #define CX_BUFFER_COPY_ON_EXTEND 0x08
 
+/**
+ * Function pointer for cxBufferWrite that is compatible with cx_write_func.
+ * @see cx_write_func
+ */
+#define cxBufferWriteFunc  ((cx_write_func) cxBufferWrite)
+/**
+ * Function pointer for cxBufferRead that is compatible with cx_read_func.
+ * @see cx_read_func
+ */
+#define cxBufferReadFunc  ((cx_read_func) cxBufferRead)
+
+/**
+ * Configuration for automatic flushing.
+ */
+struct cx_buffer_flush_config_s {
+    /**
+     * The buffer may not extend beyond this threshold before starting to flush.
+     *
+     * Only used when the buffer uses #CX_BUFFER_AUTO_EXTEND.
+     * The threshold will be the maximum capacity the buffer is extended to
+     * before flushing.
+     */
+    size_t threshold;
+    /**
+     * The block size for the elements to flush.
+     */
+    size_t blksize;
+    /**
+     * The maximum number of blocks to flush in one cycle.
+     *
+     * @attention while it is guaranteed that cxBufferFlush() will not flush
+     * more blocks, this is not necessarily the case for cxBufferWrite().
+     * After performing a flush cycle, cxBufferWrite() will retry the write
+     * operation and potentially trigger another flush cycle, until the
+     * flush target accepts no more data.
+     */
+    size_t blkmax;
+
+    /**
+     * The target for write function.
+     */
+    void *target;
+
+    /**
+     * The write-function used for flushing.
+     * If NULL, the flushed content gets discarded.
+     */
+    cx_write_func wfunc;
+};
+
+/**
+ * Type alias for the flush configuration struct.
+ *
+ * @code
+ * struct cx_buffer_flush_config_s {
+ *     size_t threshold;
+ *     size_t blksize;
+ *     size_t blkmax;
+ *     void *target;
+ *     cx_write_func wfunc;
+ * };
+ * @endcode
+ */
+typedef struct cx_buffer_flush_config_s CxBufferFlushConfig;
+
 /** Structure for the UCX buffer data. */
-typedef struct {
+struct cx_buffer_s {
     /** A pointer to the buffer contents. */
     union {
         /**
@@ -103,6 +168,12 @@
     };
     /** The allocator to use for automatic memory management. */
     const CxAllocator *allocator;
+    /**
+     * Optional flush configuration
+     *
+     * @see cxBufferEnableFlushing()
+     */
+    CxBufferFlushConfig *flush;
     /** Current position of the buffer. */
     size_t pos;
     /** Current capacity (i.e. maximum size) of the buffer. */
@@ -110,40 +181,6 @@
     /** Current size of the buffer content. */
     size_t size;
     /**
-     * The buffer may not extend beyond this threshold before starting to flush.
-     * Default is \c SIZE_MAX (flushing disabled when auto extension is enabled).
-     */
-    size_t flush_threshold;
-    /**
-     * The block size for the elements to flush.
-     * Default is 4096 bytes.
-     */
-    size_t flush_blksize;
-    /**
-     * The maximum number of blocks to flush in one cycle.
-     * Zero disables flushing entirely (this is the default).
-     * Set this to \c SIZE_MAX to flush the entire buffer.
-     *
-     * @attention if the maximum number of blocks multiplied with the block size
-     * is smaller than the expected contents written to this buffer within one write
-     * operation, multiple flush cycles are performed after that write.
-     * That means the total number of blocks flushed after one write to this buffer may
-     * be larger than \c flush_blkmax.
-     */
-    size_t flush_blkmax;
-
-    /**
-     * The write function used for flushing.
-     * If NULL, the flushed content gets discarded.
-     */
-    cx_write_func flush_func;
-
-    /**
-     * The target for \c flush_func.
-     */
-    void *flush_target;
-
-    /**
      * Flag register for buffer features.
      * @see #CX_BUFFER_DEFAULT
      * @see #CX_BUFFER_FREE_CONTENTS
@@ -151,45 +188,46 @@
      * @see #CX_BUFFER_COPY_ON_WRITE
      */
     int flags;
-} cx_buffer_s;
+};
 
 /**
  * UCX buffer.
  */
-typedef cx_buffer_s CxBuffer;
+typedef struct cx_buffer_s CxBuffer;
 
 /**
  * Initializes a fresh buffer.
  *
- * You may also provide a read-only \p space, in which case
+ * You may also provide a read-only @p space, in which case
  * you will need to cast the pointer, and you should set the
  * #CX_BUFFER_COPY_ON_WRITE flag.
  *
  * You need to set the size manually after initialization, if
- * you provide \p space which already contains data.
+ * you provide @p space which already contains data.
  *
- * When you specify stack memory as \p space and decide to use
- * the auto-extension feature, you \em must use the
+ * When you specify stack memory as @p space and decide to use
+ * the auto-extension feature, you @em must use the
  * #CX_BUFFER_COPY_ON_EXTEND flag, instead of the
  * #CX_BUFFER_AUTO_EXTEND flag.
  *
- * \note You may provide \c NULL as argument for \p space.
+ * @note You may provide @c NULL as argument for @p space.
  * Then this function will allocate the space and enforce
  * the #CX_BUFFER_FREE_CONTENTS flag. In that case, specifying
  * copy-on-write should be avoided, because the allocated
  * space will be leaking after the copy-on-write operation.
  *
  * @param buffer the buffer to initialize
- * @param space pointer to the memory area, or \c NULL to allocate
+ * @param space pointer to the memory area, or @c NULL to allocate
  * new memory
  * @param capacity the capacity of the buffer
  * @param allocator the allocator this buffer shall use for automatic
  * memory management
- * (if \c NULL, a default stdlib allocator will be used)
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param flags buffer features (see cx_buffer_s.flags)
  * @return zero on success, non-zero if a required allocation failed
  */
 cx_attr_nonnull_arg(1)
+cx_attr_export
 int cxBufferInit(
         CxBuffer *buffer,
         void *space,
@@ -199,6 +237,27 @@
 );
 
 /**
+ * Configures the buffer for flushing.
+ *
+ * Flushing can happen automatically when data is written
+ * to the buffer (see cxBufferWrite()) or manually when
+ * cxBufferFlush() is called.
+ *
+ * @param buffer the buffer
+ * @param config the flush configuration
+ * @retval zero success
+ * @retval non-zero failure
+ * @see cxBufferFlush()
+ * @see cxBufferWrite()
+ */
+cx_attr_nonnull
+cx_attr_export
+int cxBufferEnableFlushing(
+    CxBuffer *buffer,
+    CxBufferFlushConfig config
+);
+
+/**
  * Destroys the buffer contents.
  *
  * Has no effect if the #CX_BUFFER_FREE_CONTENTS feature is not enabled.
@@ -208,49 +267,52 @@
  * @see cxBufferInit()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxBufferDestroy(CxBuffer *buffer);
 
 /**
  * Deallocates the buffer.
  *
  * If the #CX_BUFFER_FREE_CONTENTS feature is enabled, this function also destroys
- * the contents. If you \em only want to destroy the contents, use cxBufferDestroy().
+ * the contents. If you @em only want to destroy the contents, use cxBufferDestroy().
  *
- * \remark As with all free() functions, this accepts \c NULL arguments in which
+ * @remark As with all free() functions, this accepts @c NULL arguments in which
  * case it does nothing.
  *
  * @param buffer the buffer to deallocate
  * @see cxBufferCreate()
  */
+cx_attr_export
 void cxBufferFree(CxBuffer *buffer);
 
 /**
  * Allocates and initializes a fresh buffer.
  *
- * You may also provide a read-only \p space, in which case
+ * You may also provide a read-only @p space, in which case
  * you will need to cast the pointer, and you should set the
  * #CX_BUFFER_COPY_ON_WRITE flag.
- * When you specify stack memory as \p space and decide to use
- * the auto-extension feature, you \em must use the
+ * When you specify stack memory as @p space and decide to use
+ * the auto-extension feature, you @em must use the
  * #CX_BUFFER_COPY_ON_EXTEND flag, instead of the
  * #CX_BUFFER_AUTO_EXTEND flag.
  *
- * \note You may provide \c NULL as argument for \p space.
+ * @note You may provide @c NULL as argument for @p space.
  * Then this function will allocate the space and enforce
  * the #CX_BUFFER_FREE_CONTENTS flag.
  *
- * @param space pointer to the memory area, or \c NULL to allocate
+ * @param space pointer to the memory area, or @c NULL to allocate
  * new memory
  * @param capacity the capacity of the buffer
  * @param allocator the allocator to use for allocating the structure and the automatic
  * memory management within the buffer
- * (if \c NULL, a default stdlib allocator will be used)
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param flags buffer features (see cx_buffer_s.flags)
- * @return a pointer to the buffer on success, \c NULL if a required allocation failed
+ * @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_attr_export
 CxBuffer *cxBufferCreate(
         void *space,
         size_t capacity,
@@ -269,7 +331,7 @@
  * are discarded.
  *
  * If the offset is negative, the contents are shifted to the left where the
- * first \p shift bytes are discarded.
+ * first @p shift bytes are discarded.
  * The new size of the buffer is the old size minus the absolute shift value.
  * If this value is larger than the buffer size, the buffer is emptied (but
  * not cleared, see the security note below).
@@ -277,11 +339,11 @@
  * The buffer position gets shifted alongside with the content but is kept
  * within the boundaries of the buffer.
  *
- * \note For situations where \c off_t is not large enough, there are specialized cxBufferShiftLeft() and
- * cxBufferShiftRight() functions using a \c size_t as parameter type.
+ * @note For situations where @c off_t is not large enough, there are specialized cxBufferShiftLeft() and
+ * cxBufferShiftRight() functions using a @c size_t as parameter type.
  *
- * \attention
- * Security Note: The shifting operation does \em not erase the previously occupied memory cells.
+ * @attention
+ * Security Note: The shifting operation does @em not erase the previously occupied memory cells.
  * But you can easily do that manually, e.g. by calling
  * <code>memset(buffer->bytes, 0, shift)</code> for a right shift or
  * <code>memset(buffer->bytes + buffer->size, 0, buffer->capacity - buffer->size)</code>
@@ -289,9 +351,13 @@
  *
  * @param buffer the buffer
  * @param shift the shift offset (negative means left shift)
- * @return 0 on success, non-zero if a required auto-extension or copy-on-write fails
+ * @retval zero success
+ * @retval non-zero if a required auto-extension or copy-on-write fails
+ * @see cxBufferShiftLeft()
+ * @see cxBufferShiftRight()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferShift(
         CxBuffer *buffer,
         off_t shift
@@ -303,10 +369,12 @@
  *
  * @param buffer the buffer
  * @param shift the shift offset
- * @return 0 on success, non-zero if a required auto-extension or copy-on-write fails
+ * @retval zero success
+ * @retval non-zero if a required auto-extension or copy-on-write fails
  * @see cxBufferShift()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferShiftRight(
         CxBuffer *buffer,
         size_t shift
@@ -318,10 +386,12 @@
  *
  * @param buffer the buffer
  * @param shift the positive shift offset
- * @return usually zero, except the buffer uses copy-on-write and the allocation fails
+ * @retval zero success
+ * @retval non-zero if the buffer uses copy-on-write and the allocation fails
  * @see cxBufferShift()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferShiftLeft(
         CxBuffer *buffer,
         size_t shift
@@ -331,23 +401,25 @@
 /**
  * Moves the position of the buffer.
  *
- * The new position is relative to the \p whence argument.
+ * The new position is relative to the @p whence argument.
  *
- * \li \c SEEK_SET marks the start of the buffer.
- * \li \c SEEK_CUR marks the current position.
- * \li \c SEEK_END marks the end of the buffer.
+ * @li @c SEEK_SET marks the start of the buffer.
+ * @li @c SEEK_CUR marks the current position.
+ * @li @c SEEK_END marks the end of the buffer.
  *
  * With an offset of zero, this function sets the buffer position to zero
- * (\c SEEK_SET), the buffer size (\c SEEK_END) or leaves the buffer position
- * unchanged (\c SEEK_CUR).
+ * (@c SEEK_SET), the buffer size (@c SEEK_END) or leaves the buffer position
+ * unchanged (@c SEEK_CUR).
  *
  * @param buffer the buffer
- * @param offset position offset relative to \p whence
- * @param whence one of \c SEEK_SET, \c SEEK_CUR or \c SEEK_END
- * @return 0 on success, non-zero if the position is invalid
+ * @param offset position offset relative to @p whence
+ * @param whence one of @c SEEK_SET, @c SEEK_CUR or @c SEEK_END
+ * @retval zero success
+ * @retval non-zero if the position is invalid
  *
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferSeek(
         CxBuffer *buffer,
         off_t offset,
@@ -360,13 +432,14 @@
  * The data is deleted by zeroing it with a call to memset().
  * If you do not need that, you can use the faster cxBufferReset().
  *
- * \note If the #CX_BUFFER_COPY_ON_WRITE flag is set, this function
+ * @note If the #CX_BUFFER_COPY_ON_WRITE flag is set, this function
  * will not erase the data and behave exactly as cxBufferReset().
  *
  * @param buffer the buffer to be cleared
  * @see cxBufferReset()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxBufferClear(CxBuffer *buffer);
 
 /**
@@ -379,17 +452,20 @@
  * @see cxBufferClear()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxBufferReset(CxBuffer *buffer);
 
 /**
  * Tests, if the buffer position has exceeded the buffer size.
  *
  * @param buffer the buffer to test
- * @return true, if the current buffer position has exceeded the last
- * byte of the buffer's contents.
+ * @retval true if the current buffer position has exceeded the last
+ * byte of the buffer's contents
+ * @retval false otherwise
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 bool cxBufferEof(const CxBuffer *buffer);
 
 
@@ -400,9 +476,11 @@
  *
  * @param buffer the buffer
  * @param capacity the minimum required capacity for this buffer
- * @return 0 on success or a non-zero value on failure
+ * @retval zero the capacity was already sufficient or successfully increased
+ * @retval non-zero on allocation failure
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferMinimumCapacity(
         CxBuffer *buffer,
         size_t capacity
@@ -411,30 +489,46 @@
 /**
  * Writes data to a CxBuffer.
  *
+ * If automatic flushing is not enabled, the data is simply written into the
+ * buffer at the current position and the position of the buffer is increased
+ * by the number of bytes written.
+ *
  * If flushing is enabled and the buffer needs to flush, the data is flushed to
  * the target until the target signals that it cannot take more data by
  * returning zero via the respective write function. In that case, the remaining
  * data in this buffer is shifted to the beginning of this buffer so that the
- * newly available space can be used to append as much data as possible. This
- * function only stops writing more elements, when the flush target and this
+ * newly available space can be used to append as much data as possible.
+ *
+ * This function only stops writing more elements, when the flush target and this
  * buffer are both incapable of taking more data or all data has been written.
- * The number returned by this function is the total number of elements that
- * could be written during the process. It does not necessarily mean that those
- * elements are still in this buffer, because some of them could have also be
- * flushed already.
+ *
+ * If, after flushing, the number of items that shall be written still exceeds
+ * the capacity or flush threshold, this function tries to write all items directly
+ * to the flush target, if possible.
  *
- * If automatic flushing is not enabled, the position of the buffer is increased
- * by the number of bytes written.
+ * The number returned by this function is the number of elements from
+ * @c ptr that could be written to either the flush target or the buffer
+ * (so it does not include the number of items that had been already in the buffer
+ * in were flushed during the process).
  *
- * \note The signature is compatible with the fwrite() family of functions.
+ * @attention
+ * When @p size is larger than one and the contents of the buffer are not aligned
+ * with @p size, flushing stops after all complete items have been flushed, leaving
+ * the mis-aligned part in the buffer.
+ * Afterward, this function only writes as many items as possible to the buffer.
+ *
+ * @note The signature is compatible with the fwrite() family of functions.
  *
  * @param ptr a pointer to the memory area containing the bytes to be written
  * @param size the length of one element
  * @param nitems the element count
  * @param buffer the CxBuffer to write to
  * @return the total count of elements written
+ * @see cxBufferAppend()
+ * @see cxBufferRead()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cxBufferWrite(
         const void *ptr,
         size_t size,
@@ -451,7 +545,7 @@
  * while additional data is added to the buffer occasionally.
  * Consequently, the position of the buffer is unchanged after this operation.
  *
- * \note The signature is compatible with the fwrite() family of functions.
+ * @note The signature is compatible with the fwrite() family of functions.
  *
  * @param ptr a pointer to the memory area containing the bytes to be written
  * @param size the length of one element
@@ -459,8 +553,10 @@
  * @param buffer the CxBuffer to write to
  * @return the total count of elements written
  * @see cxBufferWrite()
+ * @see cxBufferRead()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cxBufferAppend(
         const void *ptr,
         size_t size,
@@ -469,19 +565,79 @@
 );
 
 /**
+ * Performs a single flush-run on the specified buffer.
+ *
+ * Does nothing when the position in the buffer is zero.
+ * Otherwise, the data until the current position minus
+ * one is considered for flushing.
+ * Note carefully that flushing will never exceed the
+ * current @em position, even when the size of the
+ * buffer is larger than the current position.
+ *
+ * One flush run will try to flush @c blkmax many
+ * blocks of size @c blksize until either the @p buffer
+ * has no more data to flush or the write function
+ * used for flushing returns zero.
+ *
+ * The buffer is shifted left for that many bytes
+ * the flush operation has successfully flushed.
+ *
+ * @par Example 1
+ * Assume you have a buffer with size 340 and you are
+ * at position 200. The flush configuration is
+ * @c blkmax=4 and @c blksize=64 .
+ * Assume that the entire flush operation is successful.
+ * All 200 bytes on the left hand-side from the current
+ * position are written.
+ * That means, the size of the buffer is now 140 and the
+ * position is zero.
+ *
+ * @par Example 2
+ * Same as Example 1, but now the @c blkmax is 1.
+ * The size of the buffer is now 276 and the position is 136.
+ *
+ * @par Example 3
+ * Same as Example 1, but now assume the flush target
+ * only accepts 100 bytes before returning zero.
+ * That means, the flush operations manages to flush
+ * one complete block and one partial block, ending
+ * up with a buffer with size 240 and position 100.
+ *
+ * @remark Just returns zero when flushing was not enabled with
+ * cxBufferEnableFlushing().
+ *
+ * @remark When the buffer uses copy-on-write, the memory
+ * is copied first, before attempting any flush.
+ * This is, however, considered an erroneous use of the
+ * buffer, because it does not make much sense to put
+ * readonly data into an UCX buffer for flushing, instead
+ * of writing it directly to the target.
+ *
+ * @param buffer the buffer
+ * @return the number of successfully flushed bytes
+ * @see cxBufferEnableFlushing()
+ */
+cx_attr_nonnull
+cx_attr_export
+size_t cxBufferFlush(CxBuffer *buffer);
+
+/**
  * Reads data from a CxBuffer.
  *
  * The position of the buffer is increased by the number of bytes read.
  *
- * \note The signature is compatible with the fread() family of functions.
+ * @note The signature is compatible with the fread() family of functions.
  *
  * @param ptr a pointer to the memory area where to store the read data
  * @param size the length of one element
  * @param nitems the element count
  * @param buffer the CxBuffer to read from
  * @return the total number of elements read
+ * @see cxBufferWrite()
+ * @see cxBufferAppend()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cxBufferRead(
         void *ptr,
         size_t size,
@@ -495,25 +651,30 @@
  * The least significant byte of the argument is written to the buffer. If the
  * end of the buffer is reached and #CX_BUFFER_AUTO_EXTEND feature is enabled,
  * the buffer capacity is extended by cxBufferMinimumCapacity(). If the feature
- * is disabled or buffer extension fails, \c EOF is returned.
+ * is disabled or buffer extension fails, @c EOF is returned.
  *
  * On successful write, the position of the buffer is increased.
  *
+ * If you just want to write a null-terminator at the current position, you
+ * should use cxBufferTerminate() instead.
+ *
  * @param buffer the buffer to write to
  * @param c the character to write
- * @return the byte that has been written or \c EOF when the end of the stream is
+ * @return the byte that has been written or @c EOF when the end of the stream is
  * reached and automatic extension is not enabled or not possible
+ * @see cxBufferTerminate()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferPut(
         CxBuffer *buffer,
         int c
 );
 
 /**
- * Writes a terminating zero to a buffer.
+ * Writes a terminating zero to a buffer at the current position.
  *
- * On successful write, \em neither the position \em nor the size of the buffer is
+ * On successful write, @em neither the position @em nor the size of the buffer is
  * increased.
  *
  * The purpose of this function is to have the written data ready to be used as
@@ -523,17 +684,21 @@
  * @return zero, if the terminator could be written, non-zero otherwise
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferTerminate(CxBuffer *buffer);
 
 /**
  * Writes a string to a buffer.
  *
+ * This is a convenience function for <code>cxBufferWrite(str, 1, strlen(str), buffer)</code>.
+ *
  * @param buffer the buffer
  * @param str the zero-terminated string
  * @return the number of bytes written
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(2)
+cx_attr_export
 size_t cxBufferPutString(
         CxBuffer *buffer,
         const char *str
@@ -545,9 +710,10 @@
  * The current position of the buffer is increased after a successful read.
  *
  * @param buffer the buffer to read from
- * @return the character or \c EOF, if the end of the buffer is reached
+ * @return the character or @c EOF, if the end of the buffer is reached
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferGet(CxBuffer *buffer);
 
 #ifdef __cplusplus
--- a/ucx/cx/collection.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/collection.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file collection.h
- * \brief Common definitions for various collection implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file collection.h
+ * @brief Common definitions for various collection implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_COLLECTION_H
@@ -92,17 +92,69 @@
      * instead of copies of the actual objects.
      */
     bool store_pointer;
+    /**
+     * Indicates if this collection is guaranteed to be sorted.
+     * Note that the elements can still be sorted, even when the collection is not aware of that.
+     */
+    bool sorted;
 };
 
 /**
  * Use this macro to declare common members for a collection structure.
+ *
+ * @par Example Use
+ * @code
+ * struct MyCustomSet {
+ *     CX_COLLECTION_BASE;
+ *     MySetElements *data;
+ * }
+ * @endcode
  */
 #define CX_COLLECTION_BASE struct cx_collection_s collection
 
 /**
+ * Returns the number of elements currently stored.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @return (@c size_t) the number of currently stored elements
+ */
+#define cxCollectionSize(c) ((c)->collection.size)
+
+/**
+ * Returns the size of one element.
+ *
+ * If #cxCollectionStoresPointers() returns true, this is the size of a pointer.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @return (@c size_t) the size of one element in bytes
+ */
+#define cxCollectionElementSize(c) ((c)->collection.elem_size)
+
+/**
+ * Indicates whether this collection only stores pointers instead of the actual data.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @retval true if this collection stores only pointers to data
+ * @retval false if this collection stores the actual element's data
+ */
+#define cxCollectionStoresPointers(c) ((c)->collection.store_pointer)
+
+/**
+ * Indicates whether the collection can guarantee that the stored elements are currently sorted.
+ *
+ * This may return false even when the elements are sorted.
+ * It is totally up to the implementation of the collection whether it keeps track of the order of its elements.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @retval true if the elements are currently sorted wrt. the collection's compare function
+ * @retval false if the order of elements is unknown
+ */
+#define cxCollectionSorted(c) ((c)->collection.sorted)
+
+/**
  * Sets a simple destructor function for this collection.
  *
- * @param c the collection
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
  * @param destr the destructor function
  */
 #define cxDefineDestructor(c, destr) \
@@ -111,7 +163,7 @@
 /**
  * Sets a simple destructor function for this collection.
  *
- * @param c the collection
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
  * @param destr the destructor function
  */
 #define cxDefineAdvancedDestructor(c, destr, data) \
@@ -124,8 +176,11 @@
  * Usually only used by collection implementations. There should be no need
  * to invoke this macro manually.
  *
- * @param c the collection
- * @param e the element
+ * When the collection stores pointers, those pointers are directly passed
+ * to the destructor. Otherwise, a pointer to the element is passed.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @param e the element (the type is @c void* or @c void** depending on context)
  */
 #define cx_invoke_simple_destructor(c, e) \
     (c)->collection.simple_destructor((c)->collection.store_pointer ? (*((void **) (e))) : (e))
@@ -136,8 +191,11 @@
  * Usually only used by collection implementations. There should be no need
  * to invoke this macro manually.
  *
- * @param c the collection
- * @param e the element
+ * When the collection stores pointers, those pointers are directly passed
+ * to the destructor. Otherwise, a pointer to the element is passed.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @param e the element (the type is @c void* or @c void** depending on context)
  */
 #define cx_invoke_advanced_destructor(c, e) \
     (c)->collection.advanced_destructor((c)->collection.destructor_data, \
@@ -150,8 +208,11 @@
  * Usually only used by collection implementations. There should be no need
  * to invoke this macro manually.
  *
- * @param c the collection
- * @param e the element
+ * When the collection stores pointers, those pointers are directly passed
+ * to the destructor. Otherwise, a pointer to the element is passed.
+ *
+ * @param c a pointer to a struct that contains #CX_COLLECTION_BASE
+ * @param e the element (the type is @c void* or @c void** depending on context)
  */
 #define cx_invoke_destructor(c, e) \
     if ((c)->collection.simple_destructor) cx_invoke_simple_destructor(c,e); \
--- a/ucx/cx/common.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/common.h	Tue Feb 25 21:12:11 2025 +0100
@@ -27,15 +27,15 @@
  */
 
 /**
- * \file common.h
+ * @file common.h
  *
- * \brief Common definitions and feature checks.
+ * @brief Common definitions and feature checks.
  *
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  *
- * \mainpage UAP Common Extensions
+ * @mainpage UAP Common Extensions
  * Library with common and useful functions, macros and data structures.
  * <p>
  * Latest available source:<br>
@@ -102,6 +102,9 @@
 //       Architecture Detection
 // ---------------------------------------------------------------------------
 
+#ifndef INTPTR_MAX
+#error Missing INTPTR_MAX definition
+#endif
 #if INTPTR_MAX == INT64_MAX
 /**
  * The address width in bits on this platform.
@@ -117,19 +120,6 @@
 #endif
 
 // ---------------------------------------------------------------------------
-//       Missing Defines
-// ---------------------------------------------------------------------------
-
-#ifndef SSIZE_MAX // not defined in glibc since C23 and MSVC
-#if CX_WORDSIZE == 64
-#define SSIZE_MAX 0x7fffffffffffffffll
-#else
-#define SSIZE_MAX 0x7fffffffl
-#endif
-#endif
-
-
-// ---------------------------------------------------------------------------
 //       Attribute definitions
 // ---------------------------------------------------------------------------
 
@@ -163,10 +153,10 @@
 #ifndef __clang__
 /**
  * The pointer returned by the attributed function is supposed to be freed
- * by \p freefunc.
+ * by @p freefunc.
  *
  * @param freefunc the function that shall be used to free the memory
- * @param freefunc_arg the index of the pointer argument in \p freefunc
+ * @param freefunc_arg the index of the pointer argument in @p freefunc
  */
 #define cx_attr_dealloc(freefunc, freefunc_arg) \
     __attribute__((__malloc__(freefunc, freefunc_arg)))
@@ -190,7 +180,7 @@
 
 #ifdef __clang__
 /**
- * No support for \c null_terminated_string_arg in clang or GCC below 14.
+ * No support for @c null_terminated_string_arg in clang or GCC below 14.
  */
 #define cx_attr_cstr_arg(idx)
 /**
@@ -211,7 +201,7 @@
 #endif // __GNUC__ < 10
 #if __GNUC__ < 14
 /**
- * No support for \c null_terminated_string_arg in clang or GCC below 14.
+ * No support for @c null_terminated_string_arg in clang or GCC below 14.
  */
 #define cx_attr_cstr_arg(idx)
 #else
@@ -276,6 +266,25 @@
 
 #endif // __STDC_VERSION__
 
+
+// ---------------------------------------------------------------------------
+//       MSVC specifics
+// ---------------------------------------------------------------------------
+
+#ifdef _MSC_VER
+// fix missing _Thread_local support
+#define _Thread_local __declspec(thread)
+#endif // _MSC_VER
+
+#if defined(CX_WINDLL_EXPORT)
+#define cx_attr_export __declspec(dllexport)
+#elif defined(CX_WINDLL)
+#define cx_attr_export __declspec(dllimport)
+#else
+/** Only used for building Windows DLLs. */
+#define cx_attr_export
+#endif // CX_WINDLL / CX_WINDLL_EXPORT
+
 // ---------------------------------------------------------------------------
 //       Useful function pointers
 // ---------------------------------------------------------------------------
@@ -288,7 +297,7 @@
         size_t,
         size_t,
         void *
-) cx_attr_nonnull;
+);
 
 /**
  * Function pointer compatible with fread-like functions.
@@ -298,7 +307,7 @@
         size_t,
         size_t,
         void *
-) cx_attr_nonnull;
+);
 
 // ---------------------------------------------------------------------------
 //       Utility macros
@@ -321,66 +330,38 @@
 
 #if (__GNUC__ >= 5 || defined(__clang__)) && !defined(CX_NO_SZMUL_BUILTIN)
 #define CX_SZMUL_BUILTIN
-
-/**
- * Alias for \c __builtin_mul_overflow.
- *
- * Performs a multiplication of size_t values and checks for overflow.
- *
- * @param a first operand
- * @param b second operand
- * @param result a pointer to a size_t, where the result should
- * be stored
- * @return zero, if no overflow occurred and the result is correct, non-zero
- * otherwise
- */
 #define cx_szmul(a, b, result) __builtin_mul_overflow(a, b, result)
-
 #else // no GNUC or clang bultin
-
 /**
  * Performs a multiplication of size_t values and checks for overflow.
   *
- * @param a first operand
- * @param b second operand
- * @param result a pointer to a size_t, where the result should
+ * @param a (@c size_t) first operand
+ * @param b (@c size_t) second operand
+ * @param result (@c size_t*) a pointer to a variable, where the result should
  * be stored
- * @return zero, if no overflow occurred and the result is correct, non-zero
- * otherwise
+ * @retval zero success
+ * @retval non-zero the multiplication would overflow
  */
 #define cx_szmul(a, b, result) cx_szmul_impl(a, b, result)
 
 /**
- * Performs a multiplication of size_t values and checks for overflow.
+ * Implementation of cx_szmul() when no compiler builtin is available.
  *
- * This is a custom implementation in case there is no compiler builtin
- * available.
+ * Do not use in application code.
  *
  * @param a first operand
  * @param b second operand
- * @param result a pointer to a size_t where the result should be stored
- * @return zero, if no overflow occurred and the result is correct, non-zero
- * otherwise
+ * @param result a pointer to a variable, where the result should
+ * be stored
+ * @retval zero success
+ * @retval non-zero the multiplication would overflow
  */
 #if __cplusplus
 extern "C"
 #endif
-int cx_szmul_impl(size_t a, size_t b, size_t *result);
-
+cx_attr_export int cx_szmul_impl(size_t a, size_t b, size_t *result);
 #endif // cx_szmul
 
 
-// ---------------------------------------------------------------------------
-//       Fixes for MSVC incompatibilities
-// ---------------------------------------------------------------------------
-
-#ifdef _MSC_VER
-// fix missing ssize_t definition
-#include <BaseTsd.h>
-typedef SSIZE_T ssize_t;
-
-// fix missing _Thread_local support
-#define _Thread_local __declspec(thread)
-#endif // _MSC_VER
 
 #endif // UCX_COMMON_H
--- a/ucx/cx/compare.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/compare.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file compare.h
- * \brief A collection of simple compare functions.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file compare.h
+ * @brief A collection of simple compare functions.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_COMPARE_H
@@ -56,90 +56,117 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-typedef int(*cx_compare_func)(
-        const void *left,
-        const void *right
+cx_attr_export
+typedef int (*cx_compare_func)(
+    const void *left,
+    const void *right
 );
 
 /**
  * Compares two integers of type int.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to integer one
  * @param i2 pointer to integer two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_int(const void *i1, const void *i2);
 
 /**
- * Compares two ints.
+ * Compares two integers of type int.
  *
  * @param i1 integer one
  * @param i2 integer two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_int(int i1, int i2);
 
 /**
  * Compares two integers of type long int.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to long integer one
  * @param i2 pointer to long integer two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_longint(const void *i1, const void *i2);
 
 /**
- * Compares two long ints.
+ * Compares two integers of type long int.
  *
  * @param i1 long integer one
  * @param i2 long integer two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_longint(long int i1, long int i2);
 
 /**
  * Compares two integers of type long long.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to long long one
  * @param i2 pointer to long long two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_longlong(const void *i1, const void *i2);
 
 /**
- * Compares twolong long ints.
+ * Compares two integers of type long long.
  *
  * @param i1 long long int one
  * @param i2 long long int two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_longlong(long long int i1, long long int i2);
 
 /**
  * Compares two integers of type int16_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to int16_t one
  * @param i2 pointer to int16_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_int16(const void *i1, const void *i2);
 
 /**
@@ -147,22 +174,29 @@
  *
  * @param i1 int16_t one
  * @param i2 int16_t two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_int16(int16_t i1, int16_t i2);
 
 /**
  * Compares two integers of type int32_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to int32_t one
  * @param i2 pointer to int32_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_int32(const void *i1, const void *i2);
 
 /**
@@ -170,22 +204,29 @@
  *
  * @param i1 int32_t one
  * @param i2 int32_t two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_int32(int32_t i1, int32_t i2);
 
 /**
  * Compares two integers of type int64_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to int64_t one
  * @param i2 pointer to int64_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_int64(const void *i1, const void *i2);
 
 /**
@@ -193,91 +234,119 @@
  *
  * @param i1 int64_t one
  * @param i2 int64_t two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_int64(int64_t i1, int64_t i2);
 
 /**
  * Compares two integers of type unsigned int.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to unsigned integer one
  * @param i2 pointer to unsigned integer two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uint(const void *i1, const void *i2);
 
 /**
- * Compares two unsigned ints.
+ * Compares two integers of type unsigned int.
  *
  * @param i1 unsigned integer one
  * @param i2 unsigned integer two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_uint(unsigned int i1, unsigned int i2);
 
 /**
  * Compares two integers of type unsigned long int.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to unsigned long integer one
  * @param i2 pointer to unsigned long integer two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_ulongint(const void *i1, const void *i2);
 
 /**
- * Compares two unsigned long ints.
+ * Compares two integers of type unsigned long int.
  *
  * @param i1 unsigned long integer one
  * @param i2 unsigned long integer two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_ulongint(unsigned long int i1, unsigned long int i2);
 
 /**
  * Compares two integers of type unsigned long long.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to unsigned long long one
  * @param i2 pointer to unsigned long long two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_ulonglong(const void *i1, const void *i2);
 
 /**
- * Compares two unsigned long long ints.
+ * Compares two integers of type unsigned long long.
  *
  * @param i1 unsigned long long one
  * @param i2 unsigned long long two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_ulonglong(unsigned long long int i1, unsigned long long int i2);
 
 /**
  * Compares two integers of type uint16_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to uint16_t one
  * @param i2 pointer to uint16_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uint16(const void *i1, const void *i2);
 
 /**
@@ -285,22 +354,29 @@
  *
  * @param i1 uint16_t one
  * @param i2 uint16_t two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_uint16(uint16_t i1, uint16_t i2);
 
 /**
  * Compares two integers of type uint32_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to uint32_t one
  * @param i2 pointer to uint32_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uint32(const void *i1, const void *i2);
 
 /**
@@ -308,22 +384,29 @@
  *
  * @param i1 uint32_t one
  * @param i2 uint32_t two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_uint32(uint32_t i1, uint32_t i2);
 
 /**
  * Compares two integers of type uint64_t.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param i1 pointer to uint64_t one
  * @param i2 pointer to uint64_t two
- * @return -1, if *i1 is less than *i2, 0 if both are equal,
- * 1 if *i1 is greater than *i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uint64(const void *i1, const void *i2);
 
 /**
@@ -331,22 +414,29 @@
  *
  * @param i1 uint64_t one
  * @param i2 uint64_t two
- * @return -1, if i1 is less than i2, 0 if both are equal,
- * 1 if i1 is greater than i2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_uint64(uint64_t i1, uint64_t i2);
 
 /**
  * Compares two real numbers of type float with precision 1e-6f.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param f1 pointer to float one
  * @param f2 pointer to float two
- * @return -1, if *f1 is less than *f2, 0 if both are equal,
- * 1 if *f1 is greater than *f2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_float(const void *f1, const void *f2);
 
 /**
@@ -354,45 +444,59 @@
  *
  * @param f1 float one
  * @param f2 float two
- * @return -1, if f1 is less than f2, 0 if both are equal,
- * 1 if f1 is greater than f2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_float(float f1, float f2);
 
 /**
  * Compares two real numbers of type double with precision 1e-14.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param d1 pointer to double one
  * @param d2 pointer to double two
- * @return -1, if *d1 is less than *d2, 0 if both are equal,
- * 1 if *d1 is greater than *d2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_double(const void *d1, const void *d2);
 
 /**
- * Convenience function
+ * Compares two real numbers of type double with precision 1e-14.
  *
  * @param d1 double one
  * @param d2 double two
- * @return -1, if d1 is less than d2, 0 if both are equal,
- * 1 if d1 is greater than d2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_double(double d1, double d2);
 
 /**
  * Compares the integer representation of two pointers.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param ptr1 pointer to pointer one (const intptr_t*)
  * @param ptr2 pointer to pointer two (const intptr_t*)
- * @return -1 if *ptr1 is less than *ptr2, 0 if both are equal,
- * 1 if *ptr1 is greater than *ptr2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_intptr(const void *ptr1, const void *ptr2);
 
 /**
@@ -400,22 +504,29 @@
  *
  * @param ptr1 pointer one
  * @param ptr2 pointer two
- * @return -1 if ptr1 is less than ptr2, 0 if both are equal,
- * 1 if ptr1 is greater than ptr2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_intptr(intptr_t ptr1, intptr_t ptr2);
 
 /**
  * Compares the unsigned integer representation of two pointers.
  *
+ * @note the parameters deliberately have type @c void* to be
+ * compatible with #cx_compare_func without the need of a cast.
+ *
  * @param ptr1 pointer to pointer one (const uintptr_t*)
  * @param ptr2 pointer to pointer two (const uintptr_t*)
- * @return -1 if *ptr1 is less than *ptr2, 0 if both are equal,
- * 1 if *ptr1 is greater than *ptr2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uintptr(const void *ptr1, const void *ptr2);
 
 /**
@@ -423,22 +534,26 @@
  *
  * @param ptr1 pointer one
  * @param ptr2 pointer two
- * @return -1 if ptr1 is less than ptr2, 0 if both are equal,
- * 1 if ptr1 is greater than ptr2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_vcmp_uintptr(uintptr_t ptr1, uintptr_t ptr2);
 
 /**
- * Compares the pointers specified in the arguments without de-referencing.
+ * Compares the pointers specified in the arguments without dereferencing.
  *
  * @param ptr1 pointer one
  * @param ptr2 pointer two
- * @return -1 if ptr1 is less than ptr2, 0 if both are equal,
- * 1 if ptr1 is greater than ptr2
+ * @retval -1 if the left argument is less than the right argument
+ * @retval 0 if both arguments are equal
+ * @retval 1 if the left argument is greater than the right argument
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_ptr(const void *ptr1, const void *ptr2);
 
 #ifdef __cplusplus
--- a/ucx/cx/hash_key.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/hash_key.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file hash_key.h
- * \brief Interface for map implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file hash_key.h
+ * @brief Interface for map implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 
@@ -62,16 +62,21 @@
 typedef struct cx_hash_key_s CxHashKey;
 
 /**
- * Computes a murmur2 32 bit hash.
+ * Computes a murmur2 32-bit hash.
  *
- * You need to initialize \c data and \c len in the key struct.
+ * You need to initialize @c data and @c len in the key struct.
  * The hash is then directly written to that struct.
  *
- * \note If \c data is \c NULL, the hash is defined as 1574210520.
+ * Usually you should not need this function.
+ * Use cx_hash_key(), instead.
+ *
+ * @note If @c data is @c NULL, the hash is defined as 1574210520.
  *
  * @param key the key, the hash shall be computed for
+ * @see cx_hash_key()
  */
 cx_attr_nonnull
+cx_attr_export
 void cx_hash_murmur(CxHashKey *key);
 
 /**
@@ -84,6 +89,7 @@
  */
 cx_attr_nodiscard
 cx_attr_cstr_arg(1)
+cx_attr_export
 CxHashKey cx_hash_key_str(const char *str);
 
 /**
@@ -95,6 +101,7 @@
  */
 cx_attr_nodiscard
 cx_attr_access_r(1, 2)
+cx_attr_export
 CxHashKey cx_hash_key_bytes(
         const unsigned char *bytes,
         size_t len
@@ -113,6 +120,7 @@
  */
 cx_attr_nodiscard
 cx_attr_access_r(1, 2)
+cx_attr_export
 CxHashKey cx_hash_key(
         const void *obj,
         size_t len
@@ -132,8 +140,8 @@
 /**
  * Computes a hash key from a UCX string.
  *
- * @param str the string
- * @return the hash key
+ * @param str (@c cxstring or @c cxmutstr) the string
+ * @return (@c CxHashKey) the hash key
  */
 #define cx_hash_key_cxstr(str) cx_hash_key_cxstr(cx_strcast(str))
 
--- a/ucx/cx/hash_map.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/hash_map.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file hash_map.h
- * \brief Hash map implementation.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file hash_map.h
+ * @brief Hash map implementation.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_HASH_MAP_H
@@ -67,17 +67,17 @@
 /**
  * Creates a new hash map with the specified number of buckets.
  *
- * If \p buckets is zero, an implementation defined default will be used.
+ * If @p buckets is zero, an implementation defined default will be used.
  *
- * If \p elem_size is CX_STORE_POINTERS, the created map will be created as if
- * cxMapStorePointers() was called immediately after creation.
+ * If @p elem_size is #CX_STORE_POINTERS, the created map stores pointers instead of
+ * copies of the added elements.
  *
  * @note Iterators provided by this hash map implementation provide the remove operation.
  * The index value of an iterator is incremented when the iterator advanced without removal.
- * In other words, when the iterator is finished, \c index==size .
+ * In other words, when the iterator is finished, @c index==size .
  *
  * @param allocator the allocator to use
- * (if \c NULL, a default stdlib allocator will be used)
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param itemsize the size of one element
  * @param buckets the initial number of buckets in this hash map
  * @return a pointer to the new hash map
@@ -85,6 +85,7 @@
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxMapFree, 1)
+cx_attr_export
 CxMap *cxHashMapCreate(
         const CxAllocator *allocator,
         size_t itemsize,
@@ -94,23 +95,22 @@
 /**
  * Creates a new hash map with a default number of buckets.
  *
- * If \p elem_size is CX_STORE_POINTERS, the created map will be created as if
- * cxMapStorePointers() was called immediately after creation.
+ * If @p elem_size is #CX_STORE_POINTERS, the created map stores pointers instead of
+ * copies of the added elements.
  *
  * @note Iterators provided by this hash map implementation provide the remove operation.
  * The index value of an iterator is incremented when the iterator advanced without removal.
- * In other words, when the iterator is finished, \c index==size .
+ * In other words, when the iterator is finished, @c index==size .
  *
- * @param itemsize the size of one element
- * @return a pointer to the new hash map
+ * @param itemsize (@c size_t) the size of one element
+ * @return (@c CxMap*) a pointer to the new hash map
  */
-#define cxHashMapCreateSimple(itemsize) \
-    cxHashMapCreate(cxDefaultAllocator, itemsize, 0)
+#define cxHashMapCreateSimple(itemsize) cxHashMapCreate(NULL, itemsize, 0)
 
 /**
  * Increases the number of buckets, if necessary.
  *
- * The load threshold is \c 0.75*buckets. If the element count exceeds the load
+ * The load threshold is @c 0.75*buckets. If the element count exceeds the load
  * threshold, the map will be rehashed. Otherwise, no action is performed and
  * this function simply returns 0.
  *
@@ -123,9 +123,11 @@
  * @note If the specified map is not a hash map, the behavior is undefined.
  *
  * @param map the map to rehash
- * @return zero on success, non-zero if a memory allocation error occurred
+ * @retval zero success
+ * @retval non-zero if a memory allocation error occurred
  */
 cx_attr_nonnull
+cx_attr_export
 int cxMapRehash(CxMap *map);
 
 
--- a/ucx/cx/iterator.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/iterator.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file iterator.h
- * \brief Interface for iterator implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file iterator.h
+ * @brief Interface for iterator implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_ITERATOR_H
@@ -47,9 +47,8 @@
  */
 struct cx_iterator_base_s {
     /**
-     * True iff the iterator points to valid data.
+     * True if the iterator points to valid data.
      */
-    cx_attr_nonnull
     bool (*valid)(const void *);
 
     /**
@@ -57,15 +56,11 @@
      *
      * When valid returns false, the behavior of this function is undefined.
      */
-    cx_attr_nonnull
-    cx_attr_nodiscard
     void *(*current)(const void *);
 
     /**
      * Original implementation in case the function needs to be wrapped.
      */
-    cx_attr_nonnull
-    cx_attr_nodiscard
     void *(*current_impl)(const void *);
 
     /**
@@ -73,7 +68,6 @@
      *
      * When valid returns false, the behavior of this function is undefined.
      */
-    cx_attr_nonnull
     void (*next)(void *);
     /**
      * Indicates whether this iterator may remove elements.
@@ -86,6 +80,12 @@
 };
 
 /**
+ * Convenience type definition for the base structure of an iterator.
+ * @see #CX_ITERATOR_BASE
+ */
+typedef struct cx_iterator_base_s CxIteratorBase;
+
+/**
  * Declares base attributes for an iterator.
  * Must be the first member of an iterator structure.
  */
@@ -120,27 +120,6 @@
     } src_handle;
 
     /**
-     * Field for storing a key-value pair.
-     * May be used by iterators that iterate over k/v-collections.
-     */
-    struct {
-        /**
-         * A pointer to the key.
-         */
-        const void *key;
-        /**
-         * A pointer to the value.
-         */
-        void *value;
-    } kv_data;
-
-    /**
-     * Field for storing a slot number.
-     * May be used by iterators that iterate over multi-bucket collections.
-     */
-    size_t slot;
-
-    /**
      * If the iterator is position-aware, contains the index of the element in the underlying collection.
      * Otherwise, this field is usually uninitialized.
      */
@@ -153,7 +132,7 @@
 
     /**
      * May contain the total number of elements, if known.
-     * Shall be set to \c SIZE_MAX when the total number is unknown during iteration.
+     * Shall be set to @c SIZE_MAX when the total number is unknown during iteration.
      */
     size_t elem_count;
 };
@@ -166,7 +145,7 @@
  * to be "position-aware", which means that they keep track of the current index within the collection.
  *
  * @note Objects that are pointed to by an iterator are always mutable through that iterator. However,
- * any concurrent mutation of the collection other than by this iterator makes this iterator invalid
+ * any concurrent mutation of the collection other than by this iterator makes this iterator invalid,
  * and it must not be used anymore.
  */
 typedef struct cx_iterator_s CxIterator;
@@ -174,10 +153,9 @@
 /**
  * Checks if the iterator points to valid data.
  *
- * This is especially false for past-the-end iterators.
- *
  * @param iter the iterator
- * @return true iff the iterator points to valid data
+ * @retval true if the iterator points to valid data
+ * @retval false if the iterator already moved past the end
  */
 #define cxIteratorValid(iter) (iter).base.valid(&(iter))
 
@@ -188,6 +166,7 @@
  *
  * @param iter the iterator
  * @return a pointer to the current element
+ * @see cxIteratorValid()
  */
 #define cxIteratorCurrent(iter) (iter).base.current(&iter)
 
@@ -201,6 +180,8 @@
 /**
  * Flags the current element for removal, if this iterator is mutating.
  *
+ * Does nothing for non-mutating iterators.
+ *
  * @param iter the iterator
  */
 #define cxIteratorFlagRemoval(iter) (iter).base.remove |= (iter).base.mutating
@@ -211,11 +192,13 @@
  * 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
  * @param elem the name of the iteration variable
  * @param iter the iterator
@@ -227,21 +210,22 @@
 /**
  * Creates an iterator for the specified plain array.
  *
- * The \p array can be \c NULL in which case the iterator will be immediately
- * initialized such that #cxIteratorValid() returns \c false.
+ * The @p array can be @c NULL in which case the iterator will be immediately
+ * initialized such that #cxIteratorValid() returns @c false.
  *
  * This iterator yields the addresses of the array elements.
  * If you want to iterator over an array of pointers, you can
  * use cxIteratorPtr() to create an iterator which directly
  * yields the stored pointers.
  *
- * @param array a pointer to the array (can be \c NULL)
+ * @param array a pointer to the array (can be @c NULL)
  * @param elem_size the size of one array element
  * @param elem_count the number of elements in the array
  * @return an iterator for the specified array
  * @see cxIteratorPtr()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxIterator(
         const void *array,
         size_t elem_size,
@@ -255,23 +239,24 @@
  * elements through #cxIteratorFlagRemoval(). Every other change to the array
  * will bring this iterator to an undefined state.
  *
- * When \p remove_keeps_order is set to \c false, removing an element will only
+ * When @p remove_keeps_order is set to @c false, removing an element will only
  * move the last element to the position of the removed element, instead of
  * moving all subsequent elements by one. Usually, when the order of elements is
- * not important, this parameter should be set to \c false.
+ * not important, this parameter should be set to @c false.
  *
- * The \p array can be \c NULL in which case the iterator will be immediately
- * initialized such that #cxIteratorValid() returns \c false.
+ * The @p array can be @c NULL in which case the iterator will be immediately
+ * initialized such that #cxIteratorValid() returns @c false.
  *
  *
- * @param array a pointer to the array (can be \c NULL)
+ * @param array a pointer to the array (can be @c NULL)
  * @param elem_size the size of one array element
  * @param elem_count the number of elements in the array
- * @param remove_keeps_order \c true if the order of elements must be preserved
+ * @param remove_keeps_order @c true if the order of elements must be preserved
  * when removing an element
  * @return an iterator for the specified array
  */
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxMutIterator(
         void *array,
         size_t elem_size,
@@ -287,12 +272,13 @@
  * an iterator created with cxIterator() would return the addresses
  * of those pointers within the array).
  *
- * @param array a pointer to the array (can be \c NULL)
+ * @param array a pointer to the array (can be @c NULL)
  * @param elem_count the number of elements in the array
  * @return an iterator for the specified array
  * @see cxIterator()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxIteratorPtr(
         const void *array,
         size_t elem_count
@@ -304,15 +290,16 @@
  * This is the mutating variant of cxIteratorPtr(). See also
  * cxMutIterator().
  *
- * @param array a pointer to the array (can be \c NULL)
+ * @param array a pointer to the array (can be @c NULL)
  * @param elem_count the number of elements in the array
- * @param remove_keeps_order \c true if the order of elements must be preserved
+ * @param remove_keeps_order @c true if the order of elements must be preserved
  * when removing an element
  * @return an iterator for the specified array
  * @see cxMutIterator()
  * @see cxIteratorPtr()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxMutIteratorPtr(
         void *array,
         size_t elem_count,
--- a/ucx/cx/json.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/json.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file json.h
- * \brief Interface for parsing data from JSON files.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file json.h
+ * @brief Interface for parsing data from JSON files.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_JSON_H
@@ -146,15 +146,15 @@
  */
 enum cx_json_literal {
     /**
-     * The \c null literal.
+     * The @c null literal.
      */
     CX_JSON_NULL,
     /**
-     * The \c true literal.
+     * The @c true literal.
      */
     CX_JSON_TRUE,
     /**
-     * The \c false literal.
+     * The @c false literal.
      */
     CX_JSON_FALSE
 };
@@ -264,7 +264,7 @@
     /**
      * The type of this value.
      *
-     * Specifies how the \c value union shall be resolved.
+     * Specifies how the @c value union shall be resolved.
      */
     CxJsonValueType type;
     /**
@@ -309,7 +309,7 @@
      */
     CxJsonTokenType tokentype;
     /**
-     * True, iff the \c content must be passed to cx_strfree().
+     * True, if the @c content must be passed to cx_strfree().
      */
     bool allocated;
     /**
@@ -374,11 +374,6 @@
      * Internally reserved memory for the value buffer stack.
      */
     CxJsonValue* vbuf_internal[8];
-
-    /**
-     * Used internally.
-     */
-    bool tokenizer_escape; // TODO: check if it can be replaced with look-behind
 };
 
 /**
@@ -403,7 +398,7 @@
      * Not used as a status and never returned by any function.
      *
      * You can use this enumerator to check for all "good" status results
-     * by checking if the status is less than \c CX_JSON_OK.
+     * by checking if the status is less than @c CX_JSON_OK.
      *
      * A "good" status means, that you can refill data and continue parsing.
      */
@@ -449,6 +444,8 @@
     bool sort_members;
     /**
      * The maximum number of fractional digits in a number value.
+     * The default value is 6 and values larger than 15 are reduced to 15.
+     * Note, that the actual number of digits may be lower, depending on the concrete number.
      */
     uint8_t frac_max_digits;
     /**
@@ -457,10 +454,14 @@
      */
     bool indent_space;
     /**
-     * If \c indent_space is true, this is the number of spaces per tab.
+     * If @c indent_space is true, this is the number of spaces per tab.
      * Indentation is only used in pretty output.
      */
     uint8_t indent;
+    /**
+     * Set true to enable escaping of the slash character (solidus).
+     */
+    bool escape_slash;
 };
 
 /**
@@ -474,6 +475,7 @@
  * @return new JSON writer settings
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonWriter cxJsonWriterCompact(void);
 
 /**
@@ -483,17 +485,17 @@
  * @return new JSON writer settings
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonWriter cxJsonWriterPretty(bool use_spaces);
 
 /**
  * Writes a JSON value to a buffer or stream.
  *
- * This function blocks until all data is written or an error when trying
- * to write data occurs.
+ * This function blocks until either all data is written, or an error occurs.
  * The write operation is not atomic in the sense that it might happen
  * that the data is only partially written when an error occurs with no
  * way to indicate how much data was written.
- * To avoid this problem, you can use a CxBuffer as \p target which is
+ * To avoid this problem, you can use a CxBuffer as @p target which is
  * unlikely to fail a write operation and either use the buffer's flush
  * feature to relay the data or use the data in the buffer manually to
  * write it to the actual target.
@@ -501,10 +503,12 @@
  * @param target the buffer or stream where to write to
  * @param value the value that shall be written
  * @param wfunc the write function to use
- * @param settings formatting settings (or \c NULL to use a compact default)
- * @return zero on success, non-zero when no or not all data could be written
+ * @param settings formatting settings (or @c NULL to use a compact default)
+ * @retval zero success
+ * @retval non-zero when no or not all data could be written
  */
 cx_attr_nonnull_arg(1, 2, 3)
+cx_attr_export
 int cxJsonWrite(
     void* target,
     const CxJsonValue* value,
@@ -520,6 +524,7 @@
  * @see cxJsonDestroy()
  */
 cx_attr_nonnull_arg(1)
+cx_attr_export
 void cxJsonInit(CxJson *json, const CxAllocator *allocator);
 
 /**
@@ -529,6 +534,7 @@
  * @see cxJsonInit()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxJsonDestroy(CxJson *json);
 
 /**
@@ -560,11 +566,13 @@
  * @param json the json interface
  * @param buf the source buffer
  * @param len the length of the source buffer
- * @return zero on success, non-zero on internal allocation error
+ * @retval zero success
+ * @retval non-zero internal allocation error
  * @see cxJsonFill()
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonFilln(CxJson *json, const char *buf, size_t len);
 
 #ifdef __cplusplus
@@ -600,7 +608,7 @@
 /**
  * Fills the input buffer.
  *
- * @remark The JSON interface tries to avoid copying the input data.
+ * The JSON interface tries to avoid copying the input data.
  * When you use this function and cxJsonNext() interleaving,
  * no copies are performed. However, you must not free the
  * pointer to the data in that case. When you invoke the fill
@@ -609,8 +617,9 @@
  * an allocation of a new buffer and copying the previous contents.
  *
  * @param json the json interface
- * @param buf the source string
- * @return zero on success, non-zero on internal allocation error
+ * @param str the source string
+ * @retval zero success
+ * @retval non-zero internal allocation error
  * @see cxJsonFilln()
  */
 #define cxJsonFill(json, str) _Generic((str), \
@@ -659,18 +668,24 @@
  * Creates a new (empty) JSON object.
  *
  * @param allocator the allocator to use
- * @return the new JSON object or \c NULL if allocation fails
+ * @return the new JSON object or @c NULL if allocation fails
+ * @see cxJsonObjPutObj()
+ * @see cxJsonArrAddValues()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator);
 
 /**
  * Creates a new (empty) JSON array.
  *
  * @param allocator the allocator to use
- * @return the new JSON array or \c NULL if allocation fails
+ * @return the new JSON array or @c NULL if allocation fails
+ * @see cxJsonObjPutArr()
+ * @see cxJsonArrAddValues()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator);
 
 /**
@@ -678,11 +693,12 @@
  *
  * @param allocator the allocator to use
  * @param num the numeric value
- * @return the new JSON value or \c NULL if allocation fails
+ * @return the new JSON value or @c NULL if allocation fails
  * @see cxJsonObjPutNumber()
  * @see cxJsonArrAddNumbers()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num);
 
 /**
@@ -690,11 +706,12 @@
  *
  * @param allocator the allocator to use
  * @param num the numeric value
- * @return the new JSON value or \c NULL if allocation fails
+ * @return the new JSON value or @c NULL if allocation fails
  * @see cxJsonObjPutInteger()
  * @see cxJsonArrAddIntegers()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num);
 
 /**
@@ -702,14 +719,15 @@
  *
  * @param allocator the allocator to use
  * @param str the string data
- * @return the new JSON value or \c NULL if allocation fails
- * @see cxJsonCreateCxString()
+ * @return the new JSON value or @c NULL if allocation fails
+ * @see cxJsonCreateString()
  * @see cxJsonObjPutString()
  * @see cxJsonArrAddStrings()
  */
 cx_attr_nodiscard
 cx_attr_nonnull_arg(2)
 cx_attr_cstr_arg(2)
+cx_attr_export
 CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char *str);
 
 /**
@@ -717,12 +735,13 @@
  *
  * @param allocator the allocator to use
  * @param str the string data
- * @return the new JSON value or \c NULL if allocation fails
- * @see cxJsonCreateString()
+ * @return the new JSON value or @c NULL if allocation fails
+ * @see cxJsonCreateCxString()
  * @see cxJsonObjPutCxString()
  * @see cxJsonArrAddCxStrings()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str);
 
 /**
@@ -730,11 +749,12 @@
  *
  * @param allocator the allocator to use
  * @param lit the type of literal
- * @return the new JSON value or \c NULL if allocation fails
+ * @return the new JSON value or @c NULL if allocation fails
  * @see cxJsonObjPutLiteral()
  * @see cxJsonArrAddLiterals()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit);
 
 /**
@@ -743,10 +763,12 @@
  * @param arr the JSON array
  * @param num the array of values
  * @param count the number of elements
- * @return zero on success, non-zero on allocation failure
+ * @retval zero success
+ * @retval non-zero allocation failure
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddNumbers(CxJsonValue* arr, const double* num, size_t count);
 
 /**
@@ -755,10 +777,12 @@
  * @param arr the JSON array
  * @param num the array of values
  * @param count the number of elements
- * @return zero on success, non-zero on allocation failure
+ * @retval zero success
+ * @retval non-zero allocation failure
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count);
 
 /**
@@ -769,11 +793,13 @@
  * @param arr the JSON array
  * @param str the array of strings
  * @param count the number of elements
- * @return zero on success, non-zero on allocation failure
+ * @retval zero success
+ * @retval non-zero allocation failure
  * @see cxJsonArrAddCxStrings()
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count);
 
 /**
@@ -784,11 +810,13 @@
  * @param arr the JSON array
  * @param str the array of strings
  * @param count the number of elements
- * @return zero on success, non-zero on allocation failure
+ * @retval zero success
+ * @retval non-zero allocation failure
  * @see cxJsonArrAddStrings()
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count);
 
 /**
@@ -797,25 +825,29 @@
  * @param arr the JSON array
  * @param lit the array of literal types
  * @param count the number of elements
- * @return zero on success, non-zero on allocation failure
+ * @retval zero success
+ * @retval non-zero allocation failure
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count);
 
 /**
  * Add arbitrary values to a JSON array.
  *
- * \note In contrast to all other add functions, this function adds the values
+ * @attention In contrast to all other add functions, this function adds the values
  * directly to the array instead of copying them.
  *
  * @param arr the JSON array
  * @param val the values
  * @param count the number of elements
- * @return zero on success, non-zero on allocation failure
+ * @retval zero success
+ * @retval non-zero allocation failure
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count);
 
 /**
@@ -823,15 +855,17 @@
  *
  * The value will be directly added and not copied.
  *
- * \note If a value with the specified \p name already exists,
+ * @note If a value with the specified @p name already exists,
  * it will be (recursively) freed with its own allocator.
  *
  * @param obj the JSON object
  * @param name the name of the value
  * @param child the value
- * @return zero on success, non-zero on allocation failure
+ * @retval zero success
+ * @retval non-zero allocation failure
  */
 cx_attr_nonnull
+cx_attr_export
 int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child);
 
 /**
@@ -839,11 +873,12 @@
  *
  * @param obj the target JSON object
  * @param name the name of the new value
- * @return the new value or \c NULL if allocation fails
+ * @return the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
  * @see cxJsonCreateObj()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name);
 
 /**
@@ -851,11 +886,12 @@
  *
  * @param obj the target JSON object
  * @param name the name of the new value
- * @return the new value or \c NULL if allocation fails
+ * @return the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
  * @see cxJsonCreateArr()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name);
 
 /**
@@ -864,11 +900,12 @@
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param num the numeric value
- * @return the new value or \c NULL if allocation fails
+ * @return the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
  * @see cxJsonCreateNumber()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num);
 
 /**
@@ -877,11 +914,12 @@
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param num the numeric value
- * @return the new value or \c NULL if allocation fails
+ * @return the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
  * @see cxJsonCreateInteger()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num);
 
 /**
@@ -892,12 +930,13 @@
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param str the string data
- * @return the new value or \c NULL if allocation fails
+ * @return the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
  * @see cxJsonCreateString()
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(3)
+cx_attr_export
 CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str);
 
 /**
@@ -908,11 +947,12 @@
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param str the string data
- * @return the new value or \c NULL if allocation fails
+ * @return the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
  * @see cxJsonCreateCxString()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str);
 
 /**
@@ -921,17 +961,18 @@
  * @param obj the target JSON object
  * @param name the name of the new value
  * @param lit the type of literal
- * @return the new value or \c NULL if allocation fails
+ * @return the new value or @c NULL if allocation fails
  * @see cxJsonObjPut()
  * @see cxJsonCreateLiteral()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
 
 /**
  * Recursively deallocates the memory of a JSON value.
  *
- * \remark The type of each deallocated value will be changed
+ * @remark The type of each deallocated value will be changed
  * to #CX_JSON_NOTHING and values of such type will be skipped
  * by the de-allocation. That means, this function protects
  * you from double-frees when you are accidentally freeing
@@ -939,25 +980,42 @@
  *
  * @param value the value
  */
+cx_attr_export
 void cxJsonValueFree(CxJsonValue *value);
 
 /**
  * Tries to obtain the next JSON value.
  *
+ * Before this function can be called, the input buffer needs
+ * to be filled with cxJsonFill().
+ *
+ * When this function returns #CX_JSON_INCOMPLETE_DATA, you can
+ * add the missing data with another invocation of cxJsonFill()
+ * and then repeat the call to cxJsonNext().
  *
  * @param json the json interface
  * @param value a pointer where the next value shall be stored
- * @return a status code
+ * @retval CX_JSON_NO_ERROR successfully retrieve the @p value
+ * @retval CX_JSON_NO_DATA there is no (more) data in the buffer to read from
+ * @retval CX_JSON_INCOMPLETE_DATA an incomplete value was read
+ * and more data needs to be filled
+ * @retval CX_JSON_NULL_DATA the buffer was never initialized
+ * @retval CX_JSON_BUFFER_ALLOC_FAILED allocating internal buffer space failed
+ * @retval CX_JSON_VALUE_ALLOC_FAILED allocating memory for a CxJsonValue failed
+ * @retval CX_JSON_FORMAT_ERROR_NUMBER the JSON text contains an illegally formatted number
+ * @retval CX_JSON_FORMAT_ERROR_UNEXPECTED_TOKEN JSON syntax error
  */
 cx_attr_nonnull
 cx_attr_access_w(2)
+cx_attr_export
 CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value);
 
 /**
  * Checks if the specified value is a JSON object.
  *
  * @param value a pointer to the value
- * @return true if the value is a JSON object, false otherwise
+ * @retval true the value is a JSON object
+ * @retval false otherwise
  */
 cx_attr_nonnull
 static inline bool cxJsonIsObject(const CxJsonValue *value) {
@@ -968,7 +1026,8 @@
  * Checks if the specified value is a JSON array.
  *
  * @param value a pointer to the value
- * @return true if the value is a JSON array, false otherwise
+ * @retval true the value is a JSON array
+ * @retval false otherwise
  */
 cx_attr_nonnull
 static inline bool cxJsonIsArray(const CxJsonValue *value) {
@@ -979,7 +1038,8 @@
  * Checks if the specified value is a string.
  *
  * @param value a pointer to the value
- * @return true if the value is a string, false otherwise
+ * @retval true the value is a string
+ * @retval false otherwise
  */
 cx_attr_nonnull
 static inline bool cxJsonIsString(const CxJsonValue *value) {
@@ -993,7 +1053,8 @@
  * integer numbers.
  *
  * @param value a pointer to the value
- * @return true if the value is a JSON number, false otherwise
+ * @retval true the value is a JSON number
+ * @retval false otherwise
  * @see cxJsonIsInteger()
  */
 cx_attr_nonnull
@@ -1005,7 +1066,8 @@
  * Checks if the specified value is an integer number.
  *
  * @param value a pointer to the value
- * @return true if the value is an integer number, false otherwise
+ * @retval true the value is an integer number
+ * @retval false otherwise
  * @see cxJsonIsNumber()
  */
 cx_attr_nonnull
@@ -1016,10 +1078,11 @@
 /**
  * Checks if the specified value is a JSON literal.
  *
- * JSON literals are \c true, \c false, and \c null.
+ * JSON literals are @c true, @c false, and @c null.
  *
  * @param value a pointer to the value
- * @return true if the value is a JSON literal, false otherwise
+ * @retval true the value is a JSON literal
+ * @retval false otherwise
  * @see cxJsonIsTrue()
  * @see cxJsonIsFalse()
  * @see cxJsonIsNull()
@@ -1033,7 +1096,8 @@
  * Checks if the specified value is a Boolean literal.
  *
  * @param value a pointer to the value
- * @return true if the value is either \c true or \c false, false otherwise
+ * @retval true the value is either @c true or @c false
+ * @retval false otherwise
  * @see cxJsonIsTrue()
  * @see cxJsonIsFalse()
  */
@@ -1043,13 +1107,14 @@
 }
 
 /**
- * Checks if the specified value is \c true.
+ * Checks if the specified value is @c true.
  *
- * \remark Be advised, that this is not the same as
- * testing \c !cxJsonIsFalse(v).
+ * @remark Be advised, that this is not the same as
+ * testing @c !cxJsonIsFalse(v).
  *
  * @param value a pointer to the value
- * @return true if the value is \c true, false otherwise
+ * @retval true the value is @c true
+ * @retval false otherwise
  * @see cxJsonIsBool()
  * @see cxJsonIsFalse()
  */
@@ -1059,13 +1124,14 @@
 }
 
 /**
- * Checks if the specified value is \c false.
+ * Checks if the specified value is @c false.
  *
- * \remark Be advised, that this is not the same as
- * testing \c !cxJsonIsTrue(v).
+ * @remark Be advised, that this is not the same as
+ * testing @c !cxJsonIsTrue(v).
  *
  * @param value a pointer to the value
- * @return true if the value is \c false, false otherwise
+ * @retval true the value is @c false
+ * @retval false otherwise
  * @see cxJsonIsBool()
  * @see cxJsonIsTrue()
  */
@@ -1075,10 +1141,11 @@
 }
 
 /**
- * Checks if the specified value is \c null.
+ * Checks if the specified value is @c null.
  *
  * @param value a pointer to the value
- * @return true if the value is \c null, false otherwise
+ * @retval true the value is @c null
+ * @retval false otherwise
  * @see cxJsonIsLiteral()
  */
 cx_attr_nonnull
@@ -1089,7 +1156,7 @@
 /**
  * Obtains a C string from the given JSON value.
  *
- * If the \p value is not a string, the behavior is undefined.
+ * If the @p value is not a string, the behavior is undefined.
  *
  * @param value the JSON value
  * @return the value represented as C string
@@ -1104,7 +1171,7 @@
 /**
  * Obtains a UCX string from the given JSON value.
  *
- * If the \p value is not a string, the behavior is undefined.
+ * If the @p value is not a string, the behavior is undefined.
  *
  * @param value the JSON value
  * @return the value represented as UCX string
@@ -1118,7 +1185,7 @@
 /**
  * Obtains a mutable UCX string from the given JSON value.
  *
- * If the \p value is not a string, the behavior is undefined.
+ * If the @p value is not a string, the behavior is undefined.
  *
  * @param value the JSON value
  * @return the value represented as mutable UCX string
@@ -1132,7 +1199,7 @@
 /**
  * Obtains a double-precision floating point value from the given JSON value.
  *
- * If the \p value is not a JSON number, the behavior is undefined.
+ * If the @p value is not a JSON number, the behavior is undefined.
  *
  * @param value the JSON value
  * @return the value represented as double
@@ -1150,7 +1217,7 @@
 /**
  * Obtains a 64-bit signed integer from the given JSON value.
  *
- * If the \p value is not a JSON number, the behavior is undefined.
+ * If the @p value is not a JSON number, the behavior is undefined.
  * If it is a JSON number, but not an integer, the value will be
  * converted to an integer, possibly losing precision.
  *
@@ -1171,8 +1238,8 @@
 /**
  * Obtains a Boolean value from the given JSON value.
  *
- * If the \p value is not a JSON literal, the behavior is undefined.
- * The \c null literal is interpreted as \c false.
+ * If the @p value is not a JSON literal, the behavior is undefined.
+ * The @c null literal is interpreted as @c false.
  *
  * @param value the JSON value
  * @return the value represented as double
@@ -1186,7 +1253,7 @@
 /**
  * Returns the size of a JSON array.
  *
- * If the \p value is not a JSON array, the behavior is undefined.
+ * If the @p value is not a JSON array, the behavior is undefined.
  *
  * @param value the JSON value
  * @return the size of the array
@@ -1200,11 +1267,11 @@
 /**
  * Returns an element from a JSON array.
  *
- * If the \p value is not a JSON array, the behavior is undefined.
+ * If the @p value is not a JSON array, the behavior is undefined.
  *
  * This function guarantees to return a value. If the index is
  * out of bounds, the returned value will be of type
- * #CX_JSON_NOTHING, but never \c NULL.
+ * #CX_JSON_NOTHING, but never @c NULL.
  *
  * @param value the JSON value
  * @param index the index in the array
@@ -1213,14 +1280,15 @@
  */
 cx_attr_nonnull
 cx_attr_returns_nonnull
+cx_attr_export
 CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index);
 
 /**
  * Returns an iterator over the JSON array elements.
  *
- * The iterator yields values of type \c CxJsonValue* .
+ * The iterator yields values of type @c CxJsonValue* .
  *
- * If the \p value is not a JSON array, the behavior is undefined.
+ * If the @p value is not a JSON array, the behavior is undefined.
  *
  * @param value the JSON value
  * @return an iterator over the array elements
@@ -1228,15 +1296,16 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxJsonArrIter(const CxJsonValue *value);
 
 /**
  * Returns an iterator over the JSON object members.
  *
- * The iterator yields values of type \c CxJsonObjValue* which
+ * The iterator yields values of type @c CxJsonObjValue* which
  * contain the name and value of the member.
  *
- * If the \p value is not a JSON object, the behavior is undefined.
+ * If the @p value is not a JSON object, the behavior is undefined.
  *
  * @param value the JSON value
  * @return an iterator over the object members
@@ -1244,6 +1313,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxJsonObjIter(const CxJsonValue *value);
 
 /**
@@ -1251,20 +1321,21 @@
  */
 cx_attr_nonnull
 cx_attr_returns_nonnull
+cx_attr_export
 CxJsonValue *cx_json_obj_get_cxstr(const CxJsonValue *value, cxstring name);
 
 #ifdef __cplusplus
 } // extern "C"
 
-CxJsonValue *cxJsonObjGet(const CxJsonValue *value, cxstring name) {
+static inline CxJsonValue *cxJsonObjGet(const CxJsonValue *value, cxstring name) {
     return cx_json_obj_get_cxstr(value, name);
 }
 
-CxJsonValue *cxJsonObjGet(const CxJsonValue *value, cxmutstr name) {
+static inline CxJsonValue *cxJsonObjGet(const CxJsonValue *value, cxmutstr name) {
     return cx_json_obj_get_cxstr(value, cx_strcast(name));
 }
 
-CxJsonValue *cxJsonObjGet(const CxJsonValue *value, const char *name) {
+static inline CxJsonValue *cxJsonObjGet(const CxJsonValue *value, const char *name) {
     return cx_json_obj_get_cxstr(value, cx_str(name));
 }
 
@@ -1273,11 +1344,11 @@
 /**
  * Returns a value corresponding to a key in a JSON object.
  *
- * If the \p value is not a JSON object, the behavior is undefined.
+ * If the @p value is not a JSON object, the behavior is undefined.
  *
  * This function guarantees to return a JSON value. If the
- * object does not contain \p name, the returned JSON value
- * will be of type #CX_JSON_NOTHING, but never \c NULL.
+ * object does not contain @p name, the returned JSON value
+ * will be of type #CX_JSON_NOTHING, but never @c NULL.
  *
  * @param value the JSON object
  * @param name the key to look up
--- a/ucx/cx/linked_list.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/linked_list.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,12 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file linked_list.h
- * \brief Linked list implementation.
- * \details Also provides several low-level functions for custom linked list implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file linked_list.h
+ * @brief Linked list implementation.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_LINKED_LIST_H
@@ -45,21 +44,16 @@
 #endif
 
 /**
- * The maximum item size that uses SBO swap instead of relinking.
- */
-extern const unsigned cx_linked_list_swap_sbo_size;
-
-/**
- * Allocates a linked list for storing elements with \p elem_size bytes each.
+ * Allocates a linked list for storing elements with @p elem_size bytes each.
  *
- * If \p elem_size is CX_STORE_POINTERS, the created list will be created as if
- * cxListStorePointers() was called immediately after creation and the compare
- * function will be automatically set to cx_cmp_ptr(), if none is given.
+ * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
+ * copies of the added elements and the compare function will be automatically set
+ * to cx_cmp_ptr(), if none is given.
  *
  * @param allocator the allocator for allocating the list nodes
- * (if \c NULL, a default stdlib allocator will be used)
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param comparator the comparator for the elements
- * (if \c NULL, and the list is not storing pointers, sort and find
+ * (if @c NULL, and the list is not storing pointers, sort and find
  * functions will not work)
  * @param elem_size the size of each element in bytes
  * @return the created list
@@ -67,6 +61,7 @@
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxListFree, 1)
+cx_attr_export
 CxList *cxLinkedListCreate(
         const CxAllocator *allocator,
         cx_compare_func comparator,
@@ -74,18 +69,18 @@
 );
 
 /**
- * Allocates a linked list for storing elements with \p elem_size bytes each.
+ * Allocates a linked list for storing elements with @p elem_size bytes each.
  *
  * The list will use cxDefaultAllocator and no comparator function. If you want
  * to call functions that need a comparator, you must either set one immediately
  * after list creation or use cxLinkedListCreate().
  *
- * If \p elem_size is CX_STORE_POINTERS, the created list will be created as if
- * cxListStorePointers() was called immediately after creation and the compare
- * function will be automatically set to cx_cmp_ptr().
+ * If @p elem_size is #CX_STORE_POINTERS, the created list stores pointers instead of
+ * copies of the added elements and the compare function will be automatically set
+ * to cx_cmp_ptr(), if none is given.
  *
- * @param elem_size the size of each element in bytes
- * @return the created list
+ * @param elem_size (@c size_t) the size of each element in bytes
+ * @return (@c CxList*) the created list
  */
 #define cxLinkedListCreateSimple(elem_size) \
     cxLinkedListCreate(NULL, NULL, elem_size)
@@ -94,11 +89,11 @@
  * Finds the node at a certain index.
  *
  * This function can be used to start at an arbitrary position within the list.
- * If the search index is large than the start index, \p loc_advance must denote
- * the location of some sort of \c next pointer (i.e. a pointer to the next node).
+ * If the search index is large than the start index, @p loc_advance must denote
+ * the location of some sort of @c next pointer (i.e. a pointer to the next node).
  * But it is also possible that the search index is smaller than the start index
  * (e.g. in cases where traversing a list backwards is faster) in which case
- * \p loc_advance must denote the location of some sort of \c prev pointer
+ * @p loc_advance must denote the location of some sort of @c prev pointer
  * (i.e. a pointer to the previous node).
  *
  * @param start a pointer to the start node
@@ -109,6 +104,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 void *cx_linked_list_at(
         const void *start,
         size_t start_index,
@@ -117,59 +113,42 @@
 );
 
 /**
- * Finds the index of an element within a linked list.
+ * Finds the node containing an element within a linked list.
  *
  * @param start a pointer to the start node
  * @param loc_advance the location of the pointer to advance
- * @param loc_data the location of the \c data pointer within your node struct
- * @param cmp_func a compare function to compare \p elem against the node data
+ * @param loc_data the location of the @c data pointer within your node struct
+ * @param cmp_func a compare function to compare @p elem against the node data
  * @param elem a pointer to the element to find
- * @return the index of the element or a negative value if it could not be found
+ * @param found_index an optional pointer where the index of the found node
+ * (given that @p start has index 0) is stored
+ * @return the index of the element, if found - unspecified if not found
  */
-cx_attr_nonnull
-ssize_t cx_linked_list_find(
+cx_attr_nonnull_arg(1, 4, 5)
+cx_attr_export
+void *cx_linked_list_find(
         const void *start,
         ptrdiff_t loc_advance,
         ptrdiff_t loc_data,
         cx_compare_func cmp_func,
-        const void *elem
-);
-
-/**
- * Finds the node containing an element within a linked list.
- *
- * @param result a pointer to the memory where the node pointer (or \c NULL if the element
- * could not be found) shall be stored to
- * @param start a pointer to the start node
- * @param loc_advance the location of the pointer to advance
- * @param loc_data the location of the \c data pointer within your node struct
- * @param cmp_func a compare function to compare \p elem against the node data
- * @param elem a pointer to the element to find
- * @return the index of the element or a negative value if it could not be found
- */
-cx_attr_nonnull
-ssize_t cx_linked_list_find_node(
-        void **result,
-        const void *start,
-        ptrdiff_t loc_advance,
-        ptrdiff_t loc_data,
-        cx_compare_func cmp_func,
-        const void *elem
+        const void *elem,
+        size_t *found_index
 );
 
 /**
  * Finds the first node in a linked list.
  *
- * The function starts with the pointer denoted by \p node and traverses the list
+ * The function starts with the pointer denoted by @p node and traverses the list
  * along a prev pointer whose location within the node struct is
- * denoted by \p loc_prev.
+ * denoted by @p loc_prev.
  *
  * @param node a pointer to a node in the list
- * @param loc_prev the location of the \c prev pointer
+ * @param loc_prev the location of the @c prev pointer
  * @return a pointer to the first node
  */
 cx_attr_nonnull
 cx_attr_returns_nonnull
+cx_attr_export
 void *cx_linked_list_first(
         const void *node,
         ptrdiff_t loc_prev
@@ -178,16 +157,17 @@
 /**
  * Finds the last node in a linked list.
  *
- * The function starts with the pointer denoted by \p node and traverses the list
+ * The function starts with the pointer denoted by @p node and traverses the list
  * along a next pointer whose location within the node struct is
- * denoted by \p loc_next.
+ * denoted by @p loc_next.
  *
  * @param node a pointer to a node in the list
- * @param loc_next the location of the \c next pointer
+ * @param loc_next the location of the @c next pointer
  * @return a pointer to the last node
  */
 cx_attr_nonnull
 cx_attr_returns_nonnull
+cx_attr_export
 void *cx_linked_list_last(
         const void *node,
         ptrdiff_t loc_next
@@ -196,14 +176,15 @@
 /**
  * Finds the predecessor of a node in case it is not linked.
  *
- * \remark If \p node is not contained in the list starting with \p begin, the behavior is undefined.
+ * @remark If @p node is not contained in the list starting with @p begin, the behavior is undefined.
  *
  * @param begin the node where to start the search
- * @param loc_next the location of the \c next pointer
+ * @param loc_next the location of the @c next pointer
  * @param node the successor of the node to find
- * @return the node or \c NULL if \p node has no predecessor
+ * @return the node or @c NULL if @p node has no predecessor
  */
 cx_attr_nonnull
+cx_attr_export
 void *cx_linked_list_prev(
         const void *begin,
         ptrdiff_t loc_next,
@@ -214,15 +195,16 @@
  * Adds a new node to a linked list.
  * The node must not be part of any list already.
  *
- * \remark One of the pointers \p begin or \p end may be \c NULL, but not both.
+ * @remark One of the pointers @p begin or @p end may be @c NULL, but not both.
  *
- * @param begin a pointer to the begin node pointer (if your list has one)
+ * @param begin a pointer to the beginning node pointer (if your list has one)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param new_node a pointer to the node that shall be appended
  */
 cx_attr_nonnull_arg(5)
+cx_attr_export
 void cx_linked_list_add(
         void **begin,
         void **end,
@@ -235,15 +217,16 @@
  * Prepends a new node to a linked list.
  * The node must not be part of any list already.
  *
- * \remark One of the pointers \p begin or \p end may be \c NULL, but not both.
+ * @remark One of the pointers @p begin or @p end may be @c NULL, but not both.
  *
- * @param begin a pointer to the begin node pointer (if your list has one)
+ * @param begin a pointer to the beginning node pointer (if your list has one)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param new_node a pointer to the node that shall be prepended
  */
 cx_attr_nonnull_arg(5)
+cx_attr_export
 void cx_linked_list_prepend(
         void **begin,
         void **end,
@@ -255,12 +238,13 @@
 /**
  * Links two nodes.
  *
- * @param left the new predecessor of \p right
- * @param right the new successor of \p left
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param left the new predecessor of @p right
+ * @param right the new successor of @p left
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  */
 cx_attr_nonnull
+cx_attr_export
 void cx_linked_list_link(
         void *left,
         void *right,
@@ -273,12 +257,13 @@
  *
  * If right is not the successor of left, the behavior is undefined.
  *
- * @param left the predecessor of \p right
- * @param right the successor of \p left
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param left the predecessor of @p right
+ * @param right the successor of @p left
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  */
 cx_attr_nonnull
+cx_attr_export
 void cx_linked_list_unlink(
         void *left,
         void *right,
@@ -290,17 +275,18 @@
  * Inserts a new node after a given node of a linked list.
  * The new node must not be part of any list already.
  *
- * \note If you specify \c NULL as the \p node to insert after, this function needs either the \p begin or
- * the \p end pointer to determine the start of the list. Then the new node will be prepended to the list.
+ * @note If you specify @c NULL as the @p node to insert after, this function needs either the @p begin or
+ * the @p end pointer to determine the start of the list. Then the new node will be prepended to the list.
  *
- * @param begin a pointer to the begin node pointer (if your list has one)
+ * @param begin a pointer to the beginning node pointer (if your list has one)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
- * @param node the node after which to insert (\c NULL if you want to prepend the node to the list)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param node the node after which to insert (@c NULL if you want to prepend the node to the list)
  * @param new_node a pointer to the node that shall be inserted
  */
 cx_attr_nonnull_arg(6)
+cx_attr_export
 void cx_linked_list_insert(
         void **begin,
         void **end,
@@ -315,22 +301,23 @@
  * The chain must not be part of any list already.
  *
  * If you do not explicitly specify the end of the chain, it will be determined by traversing
- * the \c next pointer.
+ * the @c next pointer.
  *
- * \note If you specify \c NULL as the \p node to insert after, this function needs either the \p begin or
- * the \p end pointer to determine the start of the list. If only the \p end pointer is specified, you also need
- * to provide a valid \p loc_prev location.
+ * @note If you specify @c NULL as the @p node to insert after, this function needs either the @p begin or
+ * the @p end pointer to determine the start of the list. If only the @p end pointer is specified, you also need
+ * to provide a valid @p loc_prev location.
  * Then the chain will be prepended to the list.
  *
- * @param begin a pointer to the begin node pointer (if your list has one)
+ * @param begin a pointer to the beginning node pointer (if your list has one)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
- * @param node the node after which to insert (\c NULL to prepend the chain to the list)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param node the node after which to insert (@c NULL to prepend the chain to the list)
  * @param insert_begin a pointer to the first node of the chain that shall be inserted
  * @param insert_end a pointer to the last node of the chain (or NULL if the last node shall be determined)
  */
 cx_attr_nonnull_arg(6)
+cx_attr_export
 void cx_linked_list_insert_chain(
         void **begin,
         void **end,
@@ -345,17 +332,18 @@
  * Inserts a node into a sorted linked list.
  * The new node must not be part of any list already.
  *
- * If the list starting with the node pointed to by \p begin is not sorted
+ * If the list starting with the node pointed to by @p begin is not sorted
  * already, the behavior is undefined.
  *
- * @param begin a pointer to the begin node pointer (required)
+ * @param begin a pointer to the beginning node pointer (required)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param new_node a pointer to the node that shall be inserted
  * @param cmp_func a compare function that will receive the node pointers
  */
 cx_attr_nonnull_arg(1, 5, 6)
+cx_attr_export
 void cx_linked_list_insert_sorted(
         void **begin,
         void **end,
@@ -369,22 +357,23 @@
  * Inserts a chain of nodes into a sorted linked list.
  * The chain must not be part of any list already.
  *
- * If either the list starting with the node pointed to by \p begin or the list
- * starting with \p insert_begin is not sorted, the behavior is undefined.
+ * If either the list starting with the node pointed to by @p begin or the list
+ * starting with @p insert_begin is not sorted, the behavior is undefined.
  *
- * \attention In contrast to cx_linked_list_insert_chain(), the source chain
+ * @attention In contrast to cx_linked_list_insert_chain(), the source chain
  * will be broken and inserted into the target list so that the resulting list
- * will be sorted according to \p cmp_func. That means, each node in the source
+ * will be sorted according to @p cmp_func. That means, each node in the source
  * chain may be re-linked with nodes from the target list.
  *
- * @param begin a pointer to the begin node pointer (required)
+ * @param begin a pointer to the beginning node pointer (required)
  * @param end a pointer to the end node pointer (if your list has one)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param insert_begin a pointer to the first node of the chain that shall be inserted
  * @param cmp_func a compare function that will receive the node pointers
  */
 cx_attr_nonnull_arg(1, 5, 6)
+cx_attr_export
 void cx_linked_list_insert_sorted_chain(
         void **begin,
         void **end,
@@ -397,25 +386,26 @@
 /**
  * Removes a chain of nodes from the linked list.
  *
- * If one of the nodes to remove is the begin (resp. end) node of the list and if \p begin (resp. \p end)
+ * If one of the nodes to remove is the beginning (resp. end) node of the list and if @p begin (resp. @p end)
  * addresses are provided, the pointers are adjusted accordingly.
  *
  * The following combinations of arguments are valid (more arguments are optional):
- * \li \p loc_next and \p loc_prev (ancestor node is determined by using the prev pointer, overall O(1) performance)
- * \li \p loc_next and \p begin (ancestor node is determined by list traversal, overall O(n) performance)
+ * @li @p loc_next and @p loc_prev (ancestor node is determined by using the prev pointer, overall O(1) performance)
+ * @li @p loc_next and @p begin (ancestor node is determined by list traversal, overall O(n) performance)
  *
- * \remark The \c next and \c prev pointers of the removed node are not cleared by this function and may still be used
+ * @remark The @c next and @c prev pointers of the removed node are not cleared by this function and may still be used
  * to traverse to a former adjacent node in the list, or within the chain.
  *
- * @param begin a pointer to the begin node pointer (optional)
+ * @param begin a pointer to the beginning node pointer (optional)
  * @param end a pointer to the end node pointer (optional)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param node the start node of the chain
  * @param num the number of nodes to remove
- * @return the actual number of nodes that were removed (may be less when the list did not have enough nodes)
+ * @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_attr_export
 size_t cx_linked_list_remove_chain(
         void **begin,
         void **end,
@@ -428,20 +418,20 @@
 /**
  * Removes a node from the linked list.
  *
- * If the node to remove is the begin (resp. end) node of the list and if \p begin (resp. \p end)
+ * If the node to remove is the beginning (resp. end) node of the list and if @p begin (resp. @p end)
  * addresses are provided, the pointers are adjusted accordingly.
  *
  * The following combinations of arguments are valid (more arguments are optional):
- * \li \p loc_next and \p loc_prev (ancestor node is determined by using the prev pointer, overall O(1) performance)
- * \li \p loc_next and \p begin (ancestor node is determined by list traversal, overall O(n) performance)
+ * @li @p loc_next and @p loc_prev (ancestor node is determined by using the prev pointer, overall O(1) performance)
+ * @li @p loc_next and @p begin (ancestor node is determined by list traversal, overall O(n) performance)
  *
- * \remark The \c next and \c prev pointers of the removed node are not cleared by this function and may still be used
+ * @remark The @c next and @c prev pointers of the removed node are not cleared by this function and may still be used
  * to traverse to a former adjacent node in the list.
  *
- * @param begin a pointer to the begin node pointer (optional)
+ * @param begin a pointer to the beginning node pointer (optional)
  * @param end a pointer to the end node pointer (optional)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  * @param node the node to remove
  */
 cx_attr_nonnull_arg(5)
@@ -456,11 +446,14 @@
 }
 
 /**
- * Determines the size of a linked list starting with \p node.
+ * Determines the size of a linked list starting with @p node.
+ *
  * @param node the first node
- * @param loc_next the location of the \c next pointer within the node struct
- * @return the size of the list or zero if \p node is \c NULL
+ * @param loc_next the location of the @c next pointer within the node struct
+ * @return the size of the list or zero if @p node is @c NULL
  */
+cx_attr_nodiscard
+cx_attr_export
 size_t cx_linked_list_size(
         const void *node,
         ptrdiff_t loc_next
@@ -470,25 +463,26 @@
  * Sorts a linked list based on a comparison function.
  *
  * This function can work with linked lists of the following structure:
- * \code
+ * @code
  * typedef struct node node;
  * struct node {
  *   node* prev;
  *   node* next;
  *   my_payload data;
  * }
- * \endcode
+ * @endcode
  *
  * @note This is a recursive function with at most logarithmic recursion depth.
  *
- * @param begin a pointer to the begin node pointer (required)
+ * @param begin a pointer to the beginning node pointer (required)
  * @param end a pointer to the end node pointer (optional)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if not present)
- * @param loc_next the location of a \c next pointer within your node struct (required)
- * @param loc_data the location of the \c data pointer within your node struct
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if not present)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
+ * @param loc_data the location of the @c data pointer within your node struct
  * @param cmp_func the compare function defining the sort order
  */
 cx_attr_nonnull_arg(1, 6)
+cx_attr_export
 void cx_linked_list_sort(
         void **begin,
         void **end,
@@ -502,17 +496,18 @@
 /**
  * Compares two lists element wise.
  *
- * \note Both list must have the same structure.
+ * @attention Both list must have the same structure.
  *
- * @param begin_left the begin of the left list (\c NULL denotes an empty list)
- * @param begin_right the begin of the right list  (\c NULL denotes an empty list)
+ * @param begin_left the beginning of the left list (@c NULL denotes an empty list)
+ * @param begin_right the beginning of the right list  (@c NULL denotes an empty list)
  * @param loc_advance the location of the pointer to advance
- * @param loc_data the location of the \c data pointer within your node struct
+ * @param loc_data the location of the @c data pointer within your node struct
  * @param cmp_func the function to compare the elements
- * @return the first non-zero result of invoking \p cmp_func or: negative if the left list is smaller than the
+ * @return the first non-zero result of invoking @p cmp_func or: negative if the left list is smaller than the
  * right list, positive if the left list is larger than the right list, zero if both lists are equal.
  */
 cx_attr_nonnull_arg(5)
+cx_attr_export
 int cx_linked_list_compare(
         const void *begin_left,
         const void *begin_right,
@@ -524,12 +519,13 @@
 /**
  * Reverses the order of the nodes in a linked list.
  *
- * @param begin a pointer to the begin node pointer (required)
+ * @param begin a pointer to the beginning node pointer (required)
  * @param end a pointer to the end node pointer (optional)
- * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
- * @param loc_next the location of a \c next pointer within your node struct (required)
+ * @param loc_prev the location of a @c prev pointer within your node struct (negative if your struct does not have one)
+ * @param loc_next the location of a @c next pointer within your node struct (required)
  */
 cx_attr_nonnull_arg(1)
+cx_attr_export
 void cx_linked_list_reverse(
         void **begin,
         void **end,
--- a/ucx/cx/list.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/list.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file list.h
- * \brief Interface for list implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file list.h
+ * @brief Interface for list implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_LIST_H
@@ -76,14 +76,11 @@
      * Implementations SHALL invoke the content destructor functions if provided
      * and SHALL deallocate the entire list memory.
      */
-    cx_attr_nonnull
     void (*deallocate)(struct cx_list_s *list);
 
     /**
      * Member function for inserting a single element.
-     * Implementors SHOULD see to performant implementations for corner cases.
      */
-    cx_attr_nonnull
     int (*insert_element)(
             struct cx_list_s *list,
             size_t index,
@@ -92,10 +89,9 @@
 
     /**
      * Member function for inserting multiple elements.
-     * Implementors SHOULD see to performant implementations for corner cases.
+     *
      * @see cx_list_default_insert_array()
      */
-    cx_attr_nonnull
     size_t (*insert_array)(
             struct cx_list_s *list,
             size_t index,
@@ -108,7 +104,6 @@
      *
      * @see cx_list_default_insert_sorted()
      */
-    cx_attr_nonnull
     size_t (*insert_sorted)(
             struct cx_list_s *list,
             const void *sorted_data,
@@ -118,7 +113,6 @@
     /**
      * Member function for inserting an element relative to an iterator position.
      */
-    cx_attr_nonnull
     int (*insert_iter)(
             struct cx_iterator_s *iter,
             const void *elem,
@@ -128,15 +122,13 @@
     /**
      * Member function for removing elements.
      *
-     * Implementations SHALL check if \p targetbuf is set and copy the elements
+     * Implementations SHALL check if @p targetbuf is set and copy the elements
      * to the buffer without invoking any destructor.
-     * When \p targetbuf is not set, the destructors SHALL be invoked.
+     * When @p targetbuf is not set, the destructors SHALL be invoked.
      *
      * The function SHALL return the actual number of elements removed, which
-     * might be lower than \p num when going out of bounds.
+     * might be lower than @p num when going out of bounds.
      */
-    cx_attr_nonnull_arg(1)
-    cx_attr_access_w(4)
     size_t (*remove)(
             struct cx_list_s *list,
             size_t index,
@@ -147,14 +139,13 @@
     /**
      * Member function for removing all elements.
      */
-    cx_attr_nonnull
     void (*clear)(struct cx_list_s *list);
 
     /**
      * Member function for swapping two elements.
+     *
      * @see cx_list_default_swap()
      */
-    cx_attr_nonnull
     int (*swap)(
             struct cx_list_s *list,
             size_t i,
@@ -164,8 +155,6 @@
     /**
      * Member function for element lookup.
      */
-    cx_attr_nonnull
-    cx_attr_nodiscard
     void *(*at)(
             const struct cx_list_s *list,
             size_t index
@@ -174,25 +163,23 @@
     /**
      * Member function for finding and optionally removing an element.
      */
-    cx_attr_nonnull
-    cx_attr_nodiscard
-    ssize_t (*find_remove)(
+    size_t (*find_remove)(
             struct cx_list_s *list,
             const void *elem,
             bool remove
     );
 
     /**
-     * Member function for sorting the list in-place.
+     * Member function for sorting the list.
+     *
      * @see cx_list_default_sort()
      */
-    cx_attr_nonnull
     void (*sort)(struct cx_list_s *list);
 
     /**
      * Optional member function for comparing this list
      * to another list of the same type.
-     * If set to \c NULL, comparison won't be optimized.
+     * If set to @c NULL, comparison won't be optimized.
      */
     cx_attr_nonnull
     int (*compare)(
@@ -203,13 +190,11 @@
     /**
      * Member function for reversing the order of the items.
      */
-    cx_attr_nonnull
     void (*reverse)(struct cx_list_s *list);
 
     /**
      * Member function for returning an iterator pointing to the specified index.
      */
-    cx_attr_nonnull
     struct cx_iterator_s (*iterator)(
             const struct cx_list_s *list,
             size_t index,
@@ -232,6 +217,7 @@
  * @return the number of elements actually inserted
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cx_list_default_insert_array(
         struct cx_list_s *list,
         size_t index,
@@ -245,7 +231,7 @@
  * This function uses the array insert function to insert consecutive groups
  * of sorted data.
  *
- * The source data \em must already be sorted wrt. the list's compare function.
+ * The source data @em must already be sorted wrt. the list's compare function.
  *
  * Use this in your own list class if you do not want to implement an optimized
  * version for your list.
@@ -256,6 +242,7 @@
  * @return the number of elements actually inserted
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cx_list_default_insert_sorted(
         struct cx_list_s *list,
         const void *sorted_data,
@@ -274,6 +261,7 @@
  * @param list the list that shall be sorted
  */
 cx_attr_nonnull
+cx_attr_export
 void cx_list_default_sort(struct cx_list_s *list);
 
 /**
@@ -285,57 +273,74 @@
  * @param list the list in which to swap
  * @param i index of one element
  * @param j index of the other element
- * @return zero on success, non-zero when indices are out of bounds or memory
+ * @retval zero success
+ * @retval non-zero when indices are out of bounds or memory
  * allocation for the temporary buffer fails
  */
 cx_attr_nonnull
+cx_attr_export
 int cx_list_default_swap(struct cx_list_s *list, size_t i, size_t j);
 
 /**
+ * Initializes a list struct.
+ *
+ * Only use this function if you are creating your own list implementation.
+ * 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.
+ *
+ * @par Example
+ *
+ * @code
+ * CxList *myCustomListCreate(
+ *         const CxAllocator *allocator,
+ *         cx_compare_func comparator,
+ *         size_t elem_size
+ * ) {
+ *     if (allocator == NULL) {
+ *         allocator = cxDefaultAllocator;
+ *     }
+ *
+ *     MyCustomList *list = cxCalloc(allocator, 1, sizeof(MyCustomList));
+ *     if (list == NULL) return NULL;
+ *
+ *     // initialize
+ *     cx_list_init((CxList*)list, &my_custom_list_class,
+ *             allocator, comparator, elem_size);
+ *
+ *     // ... some more custom stuff ...
+ *
+ *     return (CxList *) list;
+ * }
+ * @endcode
+ *
+ * @param list the list to initialize
+ * @param cl the list class
+ * @param allocator the allocator for the elements
+ * @param comparator a compare function for the elements
+ * @param elem_size the size of one element
+ */
+cx_attr_nonnull_arg(1, 2, 3)
+cx_attr_export
+void cx_list_init(
+    struct cx_list_s *list,
+    struct cx_list_class_s *cl,
+    const struct cx_allocator_s *allocator,
+    cx_compare_func comparator,
+    size_t elem_size
+);
+
+/**
  * Common type for all list implementations.
  */
 typedef struct cx_list_s CxList;
 
 /**
- * Advises the list to store copies of the objects (default mode of operation).
- *
- * Retrieving objects from this list will yield pointers to the copies stored
- * within this list.
- *
- * @param list the list
- * @see cxListStorePointers()
- */
-cx_attr_nonnull
-void cxListStoreObjects(CxList *list);
-
-/**
- * Advises the list to only store pointers to the objects.
- *
- * Retrieving objects from this list will yield the original pointers stored.
- *
- * @note This function forcibly sets the element size to the size of a pointer.
- * Invoking this function on a non-empty list that already stores copies of
- * objects is undefined.
- *
- * @param list the list
- * @see cxListStoreObjects()
- */
-cx_attr_nonnull
-void cxListStorePointers(CxList *list);
-
-/**
- * Returns true, if this list is storing pointers instead of the actual data.
- *
- * @param list
- * @return true, if this list is storing pointers
- * @see cxListStorePointers()
- */
-cx_attr_nonnull
-static inline bool cxListIsStoringPointers(const CxList *list) {
-    return list->collection.store_pointer;
-}
-
-/**
  * Returns the number of elements currently stored in the list.
  *
  * @param list the list
@@ -351,7 +356,8 @@
  *
  * @param list the list
  * @param elem a pointer to the element to add
- * @return zero on success, non-zero on memory allocation failure
+ * @retval zero success
+ * @retval non-zero memory allocation failure
  * @see cxListAddArray()
  */
 cx_attr_nonnull
@@ -359,6 +365,7 @@
         CxList *list,
         const void *elem
 ) {
+    list->collection.sorted = false;
     return list->cl->insert_element(list, list->collection.size, elem);
 }
 
@@ -368,9 +375,9 @@
  * This method is more efficient than invoking cxListAdd() multiple times.
  *
  * If there is not enough memory to add all elements, the returned value is
- * less than \p n.
+ * less than @p n.
  *
- * If this list is storing pointers instead of objects \p array is expected to
+ * If this list is storing pointers instead of objects @p array is expected to
  * be an array of pointers.
  *
  * @param list the list
@@ -384,19 +391,20 @@
         const void *array,
         size_t n
 ) {
+    list->collection.sorted = false;
     return list->cl->insert_array(list, list->collection.size, array, n);
 }
 
 /**
  * Inserts an item at the specified index.
  *
- * If \p index equals the list \c size, this is effectively cxListAdd().
+ * If @p index equals the list @c size, this is effectively cxListAdd().
  *
  * @param list the list
  * @param index the index the element shall have
  * @param elem a pointer to the element to add
- * @return zero on success, non-zero on memory allocation failure
- * or when the index is out of bounds
+ * @retval zero success
+ * @retval non-zero memory allocation failure or the index is out of bounds
  * @see cxListInsertAfter()
  * @see cxListInsertBefore()
  */
@@ -406,36 +414,41 @@
         size_t index,
         const void *elem
 ) {
+    list->collection.sorted = false;
     return list->cl->insert_element(list, index, elem);
 }
 
 /**
  * Inserts an item into a sorted list.
  *
+ * If the list is not sorted already, the behavior is undefined.
+ *
  * @param list the list
  * @param elem a pointer to the element to add
- * @return zero on success, non-zero on memory allocation failure
+ * @retval zero success
+ * @retval non-zero memory allocation failure
  */
 cx_attr_nonnull
 static inline int cxListInsertSorted(
         CxList *list,
         const void *elem
 ) {
+    list->collection.sorted = true; // guaranteed by definition
     const void *data = list->collection.store_pointer ? &elem : elem;
     return list->cl->insert_sorted(list, data, 1) == 0;
 }
 
 /**
  * Inserts multiple items to the list at the specified index.
- * If \p index equals the list size, this is effectively cxListAddArray().
+ * If @p index equals the list size, this is effectively cxListAddArray().
  *
  * This method is usually more efficient than invoking cxListInsert()
  * multiple times.
  *
  * If there is not enough memory to add all elements, the returned value is
- * less than \p n.
+ * less than @p n.
  *
- * If this list is storing pointers instead of objects \p array is expected to
+ * If this list is storing pointers instead of objects @p array is expected to
  * be an array of pointers.
  *
  * @param list the list
@@ -451,6 +464,7 @@
         const void *array,
         size_t n
 ) {
+    list->collection.sorted = false;
     return list->cl->insert_array(list, index, array, n);
 }
 
@@ -461,11 +475,13 @@
  * because consecutive chunks of sorted data are inserted in one pass.
  *
  * If there is not enough memory to add all elements, the returned value is
- * less than \p n.
+ * less than @p n.
  *
- * If this list is storing pointers instead of objects \p array is expected to
+ * If this list is storing pointers instead of objects @p array is expected to
  * be an array of pointers.
  *
+ * If the list is not sorted already, the behavior is undefined.
+ *
  * @param list the list
  * @param array a pointer to the elements to add
  * @param n the number of elements to add
@@ -477,6 +493,7 @@
         const void *array,
         size_t n
 ) {
+    list->collection.sorted = true; // guaranteed by definition
     return list->cl->insert_sorted(list, array, n);
 }
 
@@ -486,12 +503,13 @@
  * The used iterator remains operational, but all other active iterators should
  * be considered invalidated.
  *
- * If \p iter is not a list iterator, the behavior is undefined.
- * If \p iter is a past-the-end iterator, the new element gets appended to the list.
+ * If @p iter is not a list iterator, the behavior is undefined.
+ * If @p iter is a past-the-end iterator, the new element gets appended to the list.
  *
  * @param iter an iterator
  * @param elem the element to insert
- * @return zero on success, non-zero on memory allocation failure
+ * @retval zero success
+ * @retval non-zero memory allocation failure
  * @see cxListInsert()
  * @see cxListInsertBefore()
  */
@@ -500,7 +518,9 @@
         CxIterator *iter,
         const void *elem
 ) {
-    return ((struct cx_list_s *) iter->src_handle.m)->cl->insert_iter(iter, elem, 0);
+    CxList* list = (CxList*)iter->src_handle.m;
+    list->collection.sorted = false;
+    return list->cl->insert_iter(iter, elem, 0);
 }
 
 /**
@@ -509,12 +529,13 @@
  * The used iterator remains operational, but all other active iterators should
  * be considered invalidated.
  *
- * If \p iter is not a list iterator, the behavior is undefined.
- * If \p iter is a past-the-end iterator, the new element gets appended to the list.
+ * If @p iter is not a list iterator, the behavior is undefined.
+ * If @p iter is a past-the-end iterator, the new element gets appended to the list.
  *
  * @param iter an iterator
  * @param elem the element to insert
- * @return zero on success, non-zero on memory allocation failure
+ * @retval zero success
+ * @retval non-zero memory allocation failure
  * @see cxListInsert()
  * @see cxListInsertAfter()
  */
@@ -523,7 +544,9 @@
         CxIterator *iter,
         const void *elem
 ) {
-    return ((struct cx_list_s *) iter->src_handle.m)->cl->insert_iter(iter, elem, 1);
+    CxList* list = (CxList*)iter->src_handle.m;
+    list->collection.sorted = false;
+    return list->cl->insert_iter(iter, elem, 1);
 }
 
 /**
@@ -534,7 +557,8 @@
  *
  * @param list the list
  * @param index the index of the element
- * @return zero on success, non-zero if the index is out of bounds
+ * @retval zero success
+ * @retval non-zero index out of bounds
  */
 cx_attr_nonnull
 static inline int cxListRemove(
@@ -548,12 +572,13 @@
  * Removes and returns the element at the specified index.
  *
  * No destructor is called and instead the element is copied to the
- * \p targetbuf which MUST be large enough to hold the removed element.
+ * @p targetbuf which MUST be large enough to hold the removed element.
  *
  * @param list the list
  * @param index the index of the element
  * @param targetbuf a buffer where to copy the element
- * @return zero on success, non-zero if the index is out of bounds
+ * @retval zero success
+ * @retval non-zero index out of bounds
  */
 cx_attr_nonnull
 cx_attr_access_w(3)
@@ -593,7 +618,7 @@
  * Removes and returns multiple element starting at the specified index.
  *
  * No destructor is called and instead the elements are copied to the
- * \p targetbuf which MUST be large enough to hold all removed elements.
+ * @p targetbuf which MUST be large enough to hold all removed elements.
  *
  * @param list the list
  * @param index the index of the element
@@ -615,13 +640,14 @@
 /**
  * Removes all elements from this list.
  *
- * If an element destructor function is specified, it is called for each
+ * If element destructor functions are specified, they are called for each
  * element before removing them.
  *
  * @param list the list
  */
 cx_attr_nonnull
 static inline void cxListClear(CxList *list) {
+    list->collection.sorted = true; // empty lists are always sorted
     list->cl->clear(list);
 }
 
@@ -634,7 +660,9 @@
  * @param list the list
  * @param i the index of the first element
  * @param j the index of the second element
- * @return zero on success, non-zero if one of the indices is out of bounds
+ * @retval zero success
+ * @retval non-zero one of the indices is out of bounds
+ * or the swap needed extra memory but allocation failed
  */
 cx_attr_nonnull
 static inline int cxListSwap(
@@ -642,6 +670,7 @@
         size_t i,
         size_t j
 ) {
+    list->collection.sorted = false;
     return list->cl->swap(list, i, j);
 }
 
@@ -650,7 +679,7 @@
  *
  * @param list the list
  * @param index the index of the element
- * @return a pointer to the element or \c NULL if the index is out of bounds
+ * @return a pointer to the element or @c NULL if the index is out of bounds
  */
 cx_attr_nonnull
 static inline void *cxListAt(
@@ -713,6 +742,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxListMutIteratorAt(
         CxList *list,
         size_t index
@@ -732,6 +762,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxListMutBackwardsIteratorAt(
         CxList *list,
         size_t index
@@ -803,18 +834,18 @@
 }
 
 /**
- * Returns the index of the first element that equals \p elem.
+ * Returns the index of the first element that equals @p elem.
  *
  * Determining equality is performed by the list's comparator function.
  *
  * @param list the list
  * @param elem the element to find
- * @return the index of the element or a negative
- * value when the element is not found
+ * @return the index of the element or the size of the list when the element is not found
+ * @see cxListIndexValid()
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-static inline ssize_t cxListFind(
+static inline size_t cxListFind(
         const CxList *list,
         const void *elem
 ) {
@@ -822,17 +853,32 @@
 }
 
 /**
- * Removes and returns the index of the first element that equals \p elem.
+ * Checks if the specified index is within bounds.
+ *
+ * @param list the list
+ * @param index the index
+ * @retval true if the index is within bounds
+ * @retval false if the index is out of bounds
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline bool cxListIndexValid(const CxList *list, size_t index) {
+    return index < list->collection.size;
+}
+
+/**
+ * Removes and returns the index of the first element that equals @p elem.
  *
  * Determining equality is performed by the list's comparator function.
  *
  * @param list the list
  * @param elem the element to find and remove
- * @return the index of the now removed element or a negative
- * value when the element is not found or could not be removed
+ * @return the index of the now removed element or the list size
+ * when the element is not found or could not be removed
+ * @see cxListIndexValid()
  */
 cx_attr_nonnull
-static inline ssize_t cxListFindRemove(
+static inline size_t cxListFindRemove(
         CxList *list,
         const void *elem
 ) {
@@ -840,15 +886,16 @@
 }
 
 /**
- * Sorts the list in-place.
+ * Sorts the list.
  *
- * \remark The underlying sort algorithm is implementation defined.
+ * @remark The underlying sort algorithm is implementation defined.
  *
  * @param list the list
  */
 cx_attr_nonnull
 static inline void cxListSort(CxList *list) {
     list->cl->sort(list);
+    list->collection.sorted = true;
 }
 
 /**
@@ -858,6 +905,8 @@
  */
 cx_attr_nonnull
 static inline void cxListReverse(CxList *list) {
+    // still sorted, but not according to the cmp_func
+    list->collection.sorted = false;
     list->cl->reverse(list);
 }
 
@@ -869,11 +918,15 @@
  *
  * @param list the list
  * @param other the list to compare to
- * @return zero, if both lists are equal element wise,
- * negative if the first list is smaller, positive if the first list is larger
+ * @retval zero both lists are equal element wise
+ * @retval negative the first list is smaller
+ * or the first non-equal element in the first list is smaller
+ * @retval positive the first list is larger
+ * or the first non-equal element in the first list is larger
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cxListCompare(
         const CxList *list,
         const CxList *other
@@ -882,20 +935,22 @@
 /**
  * Deallocates the memory of the specified list structure.
  *
- * Also calls the content destructor function for each element, if specified.
+ * Also calls the content destructor functions for each element, if specified.
  *
  * @param list the list which shall be freed
  */
-static inline void cxListFree(CxList *list) {
-    if (list == NULL) return;
-    list->cl->deallocate(list);
-}
+cx_attr_export
+void cxListFree(CxList *list);
 
 /**
  * A shared instance of an empty list.
  *
  * Writing to that list is not allowed.
+ *
+ * You can use this is a placeholder for initializing CxList pointers
+ * for which you do not want to reserve memory right from the beginning.
  */
+cx_attr_export
 extern CxList *const cxEmptyList;
 
 
--- a/ucx/cx/map.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/map.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file map.h
- * \brief Interface for map implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file map.h
+ * @brief Interface for map implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_MAP_H
@@ -51,6 +51,9 @@
 /** Type for a map entry. */
 typedef struct cx_map_entry_s CxMapEntry;
 
+/** Type for a map iterator. */
+typedef struct cx_map_iterator_s CxMapIterator;
+
 /** Type for map class definitions. */
 typedef struct cx_map_class_s cx_map_class;
 
@@ -65,6 +68,20 @@
 };
 
 /**
+ * A map entry.
+ */
+struct cx_map_entry_s {
+    /**
+     * A pointer to the key.
+     */
+    const CxHashKey *key;
+    /**
+     * A pointer to the value.
+     */
+    void *value;
+};
+
+/**
  * The type of iterator for a map.
  */
 enum cx_map_iterator_type {
@@ -83,25 +100,92 @@
 };
 
 /**
+ * Internal iterator struct - use CxMapIterator.
+ */
+struct cx_map_iterator_s {
+    /**
+     * Inherited common data for all iterators.
+     */
+    CX_ITERATOR_BASE;
+
+    /**
+     * Handle for the source map.
+     */
+    union {
+        /**
+         * Access for mutating iterators.
+         */
+        CxMap *m;
+        /**
+         * Access for normal iterators.
+         */
+        const CxMap *c;
+    } map;
+
+    /**
+     * Handle for the current element.
+     *
+     * @attention Depends on the map implementation, do not assume a type (better: do not use!).
+     */
+    void *elem;
+
+    /**
+     * Reserved memory for a map entry.
+     *
+     * If a map implementation uses an incompatible layout, the iterator needs something
+     * to point to during iteration which @em is compatible.
+     */
+    CxMapEntry entry;
+
+    /**
+     * Field for storing the current slot number.
+     *
+     * (Used internally)
+     */
+    size_t slot;
+
+    /**
+     * Counts the elements successfully.
+     * It usually does not denote a stable index within the map as it would be for arrays.
+     */
+    size_t index;
+
+    /**
+     * The size of a value stored in this map.
+     */
+    size_t elem_size;
+
+    /**
+     * May contain the total number of elements, if known.
+     * Set to @c SIZE_MAX when the total number is unknown during iteration.
+     *
+     * @remark The UCX implementations of #CxMap always know the number of elements they store.
+     */
+    size_t elem_count;
+
+    /**
+     * The type of this iterator.
+     */
+    enum cx_map_iterator_type type;
+};
+
+/**
  * The class definition for arbitrary maps.
  */
 struct cx_map_class_s {
     /**
      * Deallocates the entire memory.
      */
-    cx_attr_nonnull
     void (*deallocate)(struct cx_map_s *map);
 
     /**
      * Removes all elements.
      */
-    cx_attr_nonnull
     void (*clear)(struct cx_map_s *map);
 
     /**
      * Add or overwrite an element.
      */
-    cx_attr_nonnull
     int (*put)(
             CxMap *map,
             CxHashKey key,
@@ -111,8 +195,6 @@
     /**
      * Returns an element.
      */
-    cx_attr_nonnull
-    cx_attr_nodiscard
     void *(*get)(
             const CxMap *map,
             CxHashKey key
@@ -121,15 +203,13 @@
     /**
      * Removes an element.
      *
-     * Implementations SHALL check if \p targetbuf is set and copy the elements
+     * Implementations SHALL check if @p targetbuf is set and copy the elements
      * to the buffer without invoking any destructor.
-     * When \p targetbuf is not set, the destructors SHALL be invoked.
+     * When @p targetbuf is not set, the destructors SHALL be invoked.
      *
-     * The function SHALL return zero when the \p key was found and
+     * The function SHALL return zero when the @p key was found and
      * non-zero, otherwise. 
      */
-    cx_attr_nonnull_arg(1)
-    cx_attr_access_w(3)
     int (*remove)(
             CxMap *map,
             CxHashKey key,
@@ -139,90 +219,36 @@
     /**
      * Creates an iterator for this map.
      */
-    cx_attr_nonnull
-    cx_attr_nodiscard
-    CxIterator (*iterator)(const CxMap *map, enum cx_map_iterator_type type);
-};
-
-/**
- * A map entry.
- */
-struct cx_map_entry_s {
-    /**
-     * A pointer to the key.
-     */
-    const CxHashKey *key;
-    /**
-     * A pointer to the value.
-     */
-    void *value;
+    CxMapIterator (*iterator)(const CxMap *map, enum cx_map_iterator_type type);
 };
 
 /**
  * A shared instance of an empty map.
  *
  * Writing to that map is not allowed.
- */
-extern CxMap *const cxEmptyMap;
-
-/**
- * Advises the map to store copies of the objects (default mode of operation).
  *
- * Retrieving objects from this map will yield pointers to the copies stored
- * within this list.
- *
- * @param map the map
- * @see cxMapStorePointers()
+ * You can use this is a placeholder for initializing CxMap pointers
+ * for which you do not want to reserve memory right from the beginning.
  */
-cx_attr_nonnull
-static inline void cxMapStoreObjects(CxMap *map) {
-    map->collection.store_pointer = false;
-}
-
-/**
- * Advises the map to only store pointers to the objects.
- *
- * Retrieving objects from this list will yield the original pointers stored.
- *
- * @note This function forcibly sets the element size to the size of a pointer.
- * Invoking this function on a non-empty map that already stores copies of
- * objects is undefined.
- *
- * @param map the map
- * @see cxMapStoreObjects()
- */
-cx_attr_nonnull
-static inline void cxMapStorePointers(CxMap *map) {
-    map->collection.store_pointer = true;
-    map->collection.elem_size = sizeof(void *);
-}
-
-/**
- * Returns true, if this map is storing pointers instead of the actual data.
- *
- * @param map
- * @return true, if this map is storing pointers
- * @see cxMapStorePointers()
- */
-cx_attr_nonnull
-static inline bool cxMapIsStoringPointers(const CxMap *map) {
-    return map->collection.store_pointer;
-}
+cx_attr_export
+extern CxMap *const cxEmptyMap;
 
 /**
  * Deallocates the memory of the specified map.
  *
+ * Also calls the content destructor functions for each element, if specified.
+ *
  * @param map the map to be freed
  */
-static inline void cxMapFree(CxMap *map) {
-    if (map == NULL) return;
-    map->cl->deallocate(map);
-}
+cx_attr_export
+void cxMapFree(CxMap *map);
 
 
 /**
  * Clears a map by removing all elements.
  *
+ * Also calls the content destructor functions for each element, if specified.
+ *
  * @param map the map to be cleared
  */
 cx_attr_nonnull
@@ -241,13 +267,14 @@
     return map->collection.size;
 }
 
-
-// TODO: set-like map operations (union, intersect, difference)
-
 /**
  * Creates a value iterator for a map.
  *
- * \note An iterator iterates over all elements successively. Therefore, the order
+ * When the map is storing pointers, those pointers are returned.
+ * Otherwise, the iterator iterates over pointers to the memory within the map where the
+ * respective elements are stored.
+ *
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
@@ -255,16 +282,17 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-static inline CxIterator cxMapIteratorValues(const CxMap *map) {
+static inline CxMapIterator cxMapIteratorValues(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_VALUES);
 }
 
 /**
  * Creates a key iterator for a map.
  *
- * The elements of the iterator are keys of type CxHashKey.
+ * The elements of the iterator are keys of type CxHashKey and the pointer returned
+ * during iterator shall be treated as @c const @c CxHashKey* .
  *
- * \note An iterator iterates over all elements successively. Therefore, the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
@@ -272,16 +300,17 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-static inline CxIterator cxMapIteratorKeys(const CxMap *map) {
+static inline CxMapIterator cxMapIteratorKeys(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_KEYS);
 }
 
 /**
  * Creates an iterator for a map.
  *
- * The elements of the iterator are key/value pairs of type CxMapEntry.
+ * The elements of the iterator are key/value pairs of type CxMapEntry and the pointer returned
+ * during iterator shall be treated as @c const @c CxMapEntry* .
  *
- * \note An iterator iterates over all elements successively. Therefore, the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
@@ -291,7 +320,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-static inline CxIterator cxMapIterator(const CxMap *map) {
+static inline CxMapIterator cxMapIterator(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_PAIRS);
 }
 
@@ -299,7 +328,11 @@
 /**
  * Creates a mutating iterator over the values of a map.
  *
- * \note An iterator iterates over all elements successively. Therefore, the order
+ * When the map is storing pointers, those pointers are returned.
+ * Otherwise, the iterator iterates over pointers to the memory within the map where the
+ * respective elements are stored.
+ *
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
@@ -307,14 +340,16 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-CxIterator cxMapMutIteratorValues(CxMap *map);
+cx_attr_export
+CxMapIterator cxMapMutIteratorValues(CxMap *map);
 
 /**
  * Creates a mutating iterator over the keys of a map.
  *
- * The elements of the iterator are keys of type CxHashKey.
+ * The elements of the iterator are keys of type CxHashKey and the pointer returned
+ * during iterator shall be treated as @c const @c CxHashKey* .
  *
- * \note An iterator iterates over all elements successively. Therefore, the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
@@ -322,14 +357,16 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-CxIterator cxMapMutIteratorKeys(CxMap *map);
+cx_attr_export
+CxMapIterator cxMapMutIteratorKeys(CxMap *map);
 
 /**
  * Creates a mutating iterator for a map.
  *
- * The elements of the iterator are key/value pairs of type CxMapEntry.
+ * The elements of the iterator are key/value pairs of type CxMapEntry and the pointer returned
+ * during iterator shall be treated as @c const @c CxMapEntry* .
  *
- * \note An iterator iterates over all elements successively. Therefore, the order
+ * @note An iterator iterates over all elements successively. Therefore, the order
  * highly depends on the map implementation and may change arbitrarily when the contents change.
  *
  * @param map the map to create the iterator for
@@ -339,19 +376,11 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
-CxIterator cxMapMutIterator(CxMap *map);
+cx_attr_export
+CxMapIterator cxMapMutIterator(CxMap *map);
 
 #ifdef __cplusplus
 } // end the extern "C" block here, because we want to start overloading
-
-/**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
- */
 cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
@@ -361,15 +390,6 @@
     return map->cl->put(map, key, value);
 }
 
-
-/**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
- */
 cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
@@ -379,14 +399,6 @@
     return map->cl->put(map, cx_hash_key_cxstr(key), value);
 }
 
-/**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
- */
 cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
@@ -396,14 +408,6 @@
     return map->cl->put(map, cx_hash_key_cxstr(key), value);
 }
 
-/**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
- */
 cx_attr_nonnull
 cx_attr_cstr_arg(2)
 static inline int cxMapPut(
@@ -414,13 +418,6 @@
     return map->cl->put(map, cx_hash_key_str(key), value);
 }
 
-/**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
- */
 cx_attr_nonnull
 cx_attr_nodiscard
 static inline void *cxMapGet(
@@ -430,13 +427,6 @@
     return map->cl->get(map, key);
 }
 
-/**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
- */
 cx_attr_nonnull
 cx_attr_nodiscard
 static inline void *cxMapGet(
@@ -446,13 +436,6 @@
     return map->cl->get(map, cx_hash_key_cxstr(key));
 }
 
-/**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
- */
 cx_attr_nonnull
 cx_attr_nodiscard
 static inline void *cxMapGet(
@@ -462,13 +445,6 @@
     return map->cl->get(map, cx_hash_key_cxstr(key));
 }
 
-/**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
- */
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_cstr_arg(2)
@@ -479,17 +455,6 @@
     return map->cl->get(map, cx_hash_key_str(key));
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructors functions, if any, on the removed element.
- *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapRemoveAndGet()
- */
 cx_attr_nonnull
 static inline int cxMapRemove(
         CxMap *map,
@@ -498,17 +463,6 @@
     return map->cl->remove(map, key, nullptr);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructors functions, if any, on the removed element.
- *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapRemoveAndGet()
- */
 cx_attr_nonnull
 static inline int cxMapRemove(
         CxMap *map,
@@ -517,17 +471,6 @@
     return map->cl->remove(map, cx_hash_key_cxstr(key), nullptr);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructors functions, if any, on the removed element.
- *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapRemoveAndGet()
- */
 cx_attr_nonnull
 static inline int cxMapRemove(
         CxMap *map,
@@ -536,17 +479,6 @@
     return map->cl->remove(map, cx_hash_key_cxstr(key), nullptr);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructors functions, if any, on the removed element.
- *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapRemoveAndGet()
- */
 cx_attr_nonnull
 cx_attr_cstr_arg(2)
 static inline int cxMapRemove(
@@ -556,24 +488,6 @@
     return map->cl->remove(map, cx_hash_key_str(key), nullptr);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
- *
- * If this map is storing pointers, the element is the pointer itself
- * and not the object it points to.
- *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapStorePointers()
- * @see cxMapRemove()
- */
 cx_attr_nonnull
 cx_attr_access_w(3)
 static inline int cxMapRemoveAndGet(
@@ -584,24 +498,6 @@
     return map->cl->remove(map, key, targetbuf);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
- *
- * If this map is storing pointers, the element is the pointer itself
- * and not the object it points to.
- *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapStorePointers()
- * @see cxMapRemove()
- */
 cx_attr_nonnull
 cx_attr_access_w(3)
 static inline int cxMapRemoveAndGet(
@@ -612,24 +508,6 @@
     return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
- *
- * If this map is storing pointers, the element is the pointer itself
- * and not the object it points to.
- *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapStorePointers()
- * @see cxMapRemove()
- */
 cx_attr_nonnull
 cx_attr_access_w(3)
 static inline int cxMapRemoveAndGet(
@@ -640,24 +518,6 @@
     return map->cl->remove(map, cx_hash_key_cxstr(key), targetbuf);
 }
 
-/**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
- *
- * If this map is storing pointers, the element is the pointer itself
- * and not the object it points to.
- *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapStorePointers()
- * @see cxMapRemove()
- */
 cx_attr_nonnull
 cx_attr_access_w(3)
 cx_attr_cstr_arg(2)
@@ -672,12 +532,7 @@
 #else // __cplusplus
 
 /**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * @copydoc cxMapPut()
  */
 cx_attr_nonnull
 static inline int cx_map_put(
@@ -689,12 +544,7 @@
 }
 
 /**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * @copydoc cxMapPut()
  */
 cx_attr_nonnull
 static inline int cx_map_put_cxstr(
@@ -706,12 +556,7 @@
 }
 
 /**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * @copydoc cxMapPut()
  */
 cx_attr_nonnull
 static inline int cx_map_put_mustr(
@@ -723,12 +568,7 @@
 }
 
 /**
- * Puts a key/value-pair into the map.
- *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * @copydoc cxMapPut()
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(2)
@@ -743,10 +583,21 @@
 /**
  * Puts a key/value-pair into the map.
  *
- * @param map the map
- * @param key the key
- * @param value the value
- * @return 0 on success, non-zero value on failure
+ * A possible existing value will be overwritten.
+ * If destructor functions are specified, they are called for
+ * the overwritten element.
+ *
+ * If this map is storing pointers, the @p value pointer is written
+ * to the map. Otherwise, the memory is copied from @p value with
+ * memcpy().
+ *
+ * The @p key is always copied.
+ *
+ * @param map (@c CxMap*) the map
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param value (@c void*) the value
+ * @retval zero success
+ * @retval non-zero value on memory allocation failure
  */
 #define cxMapPut(map, key, value) _Generic((key), \
     CxHashKey: cx_map_put,                        \
@@ -757,11 +608,7 @@
     (map, key, value)
 
 /**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
+ * @copydoc cxMapGet()
  */
 cx_attr_nonnull
 cx_attr_nodiscard
@@ -773,11 +620,7 @@
 }
 
 /**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
+ * @copydoc cxMapGet()
  */
 cx_attr_nonnull
 cx_attr_nodiscard
@@ -789,11 +632,7 @@
 }
 
 /**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
+ * @copydoc cxMapGet()
  */
 cx_attr_nonnull
 cx_attr_nodiscard
@@ -805,11 +644,7 @@
 }
 
 /**
- * Retrieves a value by using a key.
- *
- * @param map the map
- * @param key the key
- * @return the value
+ * @copydoc cxMapGet()
  */
 cx_attr_nonnull
 cx_attr_nodiscard
@@ -824,9 +659,13 @@
 /**
  * Retrieves a value by using a key.
  *
- * @param map the map
- * @param key the key
- * @return the value
+ * If this map is storing pointers, the stored pointer is returned.
+ * Otherwise, a pointer to the element within the map's memory
+ * is returned (which is valid as long as the element stays in the map).
+ *
+ * @param map (@c CxMap*) the map
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @return (@c void*) the value
  */
 #define cxMapGet(map, key) _Generic((key), \
     CxHashKey: cx_map_get,                 \
@@ -837,13 +676,7 @@
     (map, key)
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructors functions, if any, on the removed element.
- *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
+ * @copydoc cxMapRemove()
  */
 cx_attr_nonnull
 static inline int cx_map_remove(
@@ -854,13 +687,7 @@
 }
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructors functions, if any, on the removed element.
- *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
+ * @copydoc cxMapRemove()
  */
 cx_attr_nonnull
 static inline int cx_map_remove_cxstr(
@@ -871,13 +698,7 @@
 }
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructors functions, if any, on the removed element.
- *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
+ * @copydoc cxMapRemove()
  */
 cx_attr_nonnull
 static inline int cx_map_remove_mustr(
@@ -888,13 +709,7 @@
 }
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * Always invokes the destructors functions, if any, on the removed element.
- *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
+ * @copydoc cxMapRemove()
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(2)
@@ -910,9 +725,10 @@
  *
  * Always invokes the destructors functions, if any, on the removed element.
  *
- * @param map the map
- * @param key the key
- * @return zero on success, non-zero if the key was not found
+ * @param map (@c CxMap*) the map
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @retval zero success
+ * @retval non-zero the key was not found
  * 
  * @see cxMapRemoveAndGet()
  */
@@ -925,19 +741,7 @@
     (map, key)
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
- *
- * If this map is storing pointers, the element is the pointer itself
- * and not the object it points to.
- *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
+ * @copydoc cxMapRemoveAndGet()
  */
 cx_attr_nonnull
 cx_attr_access_w(3)
@@ -950,19 +754,7 @@
 }
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
- *
- * If this map is storing pointers, the element is the pointer itself
- * and not the object it points to.
- *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
+ * @copydoc cxMapRemoveAndGet()
  */
 cx_attr_nonnull
 cx_attr_access_w(3)
@@ -975,19 +767,7 @@
 }
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
- *
- * If this map is storing pointers, the element is the pointer itself
- * and not the object it points to.
- *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
+ * @copydoc cxMapRemoveAndGet()
  */
 cx_attr_nonnull
 cx_attr_access_w(3)
@@ -1000,19 +780,7 @@
 }
 
 /**
- * Removes a key/value-pair from the map by using the key.
- *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
- *
- * If this map is storing pointers, the element is the pointer itself
- * and not the object it points to.
- *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
+ * @copydoc cxMapRemoveAndGet()
  */
 cx_attr_nonnull
 cx_attr_access_w(3)
@@ -1028,19 +796,20 @@
 /**
  * Removes a key/value-pair from the map by using the key.
  *
- * This function will copy the contents to the target buffer
- * which must be guaranteed to be large enough to hold the element.
- * The destructor functions, if any, will \em not be called.
+ * This function will copy the contents of the removed element
+ * to the target buffer, which must be guaranteed to be large enough
+ * to hold the element (the map's element size).
+ * The destructor functions, if any, will @em not be called.
  *
  * If this map is storing pointers, the element is the pointer itself
  * and not the object it points to.
  *
- * @param map the map
- * @param key the key
- * @param targetbuf the buffer where the element shall be copied to
- * @return zero on success, non-zero if the key was not found
- * 
- * @see cxMapStorePointers()
+ * @param map (@c CxMap*) the map
+ * @param key (@c CxHashKey, @c char*, @c cxstring, or @c cxmutstr) the key
+ * @param targetbuf (@c void*) the buffer where the element shall be copied to
+ * @retval zero success
+ * @retval non-zero the key was not found
+ *
  * @see cxMapRemove()
  */
 #define cxMapRemoveAndGet(map, key, targetbuf) _Generic((key), \
--- a/ucx/cx/mempool.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/mempool.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file mempool.h
- * \brief Interface for memory pool implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file mempool.h
+ * @brief Interface for memory pool implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_MEMPOOL_H
@@ -80,6 +80,7 @@
  *
  * @param pool the memory pool to free
  */
+cx_attr_export
 void cxMempoolFree(CxMempool *pool);
 
 /**
@@ -89,20 +90,21 @@
  *
  * @param capacity the initial capacity of the pool
  * @param destr optional destructor function to use for allocated memory
- * @return the created memory pool or \c NULL if allocation failed
+ * @return the created memory pool or @c NULL if allocation failed
  */
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxMempoolFree, 1)
+cx_attr_export
 CxMempool *cxMempoolCreate(size_t capacity, cx_destructor_func destr);
 
 /**
  * Creates a basic array-based memory pool.
  *
- * @param capacity the initial capacity of the pool
- * @return the created memory pool or \c NULL if allocation failed
+ * @param capacity (@c size_t) the initial capacity of the pool
+ * @return (@c CxMempool*) the created memory pool or @c NULL if allocation failed
  */
-#define cxBasicMempoolCreate(capacity) cxMempoolCreate(capacity, NULL)
+#define cxMempoolCreateSimple(capacity) cxMempoolCreate(capacity, NULL)
 
 /**
  * Sets the destructor function for a specific allocated memory object.
@@ -114,6 +116,7 @@
  * @param fnc the destructor function
  */
 cx_attr_nonnull
+cx_attr_export
 void cxMempoolSetDestructor(
         void *memory,
         cx_destructor_func fnc
@@ -128,6 +131,7 @@
  * @param memory the object allocated in the pool
  */
 cx_attr_nonnull
+cx_attr_export
 void cxMempoolRemoveDestructor(void *memory);
 
 /**
@@ -141,9 +145,11 @@
  * @param pool the pool
  * @param memory the object to register (MUST NOT be already allocated in the pool)
  * @param destr the destructor function
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  */
 cx_attr_nonnull
+cx_attr_export
 int cxMempoolRegister(
         CxMempool *pool,
         void *memory,
--- a/ucx/cx/printf.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/printf.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file printf.h
- * \brief Wrapper for write functions with a printf-like interface.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file printf.h
+ * @brief Wrapper for write functions with a printf-like interface.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_PRINTF_H
@@ -56,21 +56,23 @@
 /**
  * The maximum string length that fits into stack memory.
  */
+cx_attr_export
 extern const unsigned cx_printf_sbo_size;
 
 /**
- * A \c fprintf like function which writes the output to a stream by
+ * A @c fprintf like function which writes the output to a stream by
  * using a write_func.
  *
  * @param stream the stream the data is written to
  * @param wfc the write function
  * @param fmt format string
  * @param ... additional arguments
- * @return the total number of bytes written
+ * @return the total number of bytes written or an error code from stdlib printf implementation
  */
 cx_attr_nonnull_arg(1, 2, 3)
 cx_attr_printf(3, 4)
 cx_attr_cstr_arg(3)
+cx_attr_export
 int cx_fprintf(
         void *stream,
         cx_write_func wfc,
@@ -79,18 +81,19 @@
 );
 
 /**
- * A \c vfprintf like function which writes the output to a stream by
+ * A @c vfprintf like function which writes the output to a stream by
  * using a write_func.
  *
  * @param stream the stream the data is written to
  * @param wfc the write function
  * @param fmt format string
  * @param ap argument list
- * @return the total number of bytes written
+ * @return the total number of bytes written or an error code from stdlib printf implementation
  * @see cx_fprintf()
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(3)
+cx_attr_export
 int cx_vfprintf(
         void *stream,
         cx_write_func wfc,
@@ -99,10 +102,12 @@
 );
 
 /**
- * A \c asprintf like function which allocates space for a string
+ * A @c asprintf like function which allocates space for a string
  * the result is written to.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated,
+ * unless there was an error, in which case the string's pointer
+ * will be @c NULL.
  *
  * @param allocator the CxAllocator used for allocating the string
  * @param fmt format string
@@ -113,6 +118,7 @@
 cx_attr_nonnull_arg(1, 2)
 cx_attr_printf(2, 3)
 cx_attr_cstr_arg(2)
+cx_attr_export
 cxmutstr cx_asprintf_a(
         const CxAllocator *allocator,
         const char *fmt,
@@ -120,24 +126,28 @@
 );
 
 /**
- * A \c asprintf like function which allocates space for a string
+ * A @c asprintf like function which allocates space for a string
  * the result is written to.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated,
+ * unless there was an error, in which case the string's pointer
+ * will be @c NULL.
  *
- * @param fmt format string
+ * @param fmt (@c char*) format string
  * @param ... additional arguments
- * @return the formatted string
+ * @return (@c cxmutstr) the formatted string
  * @see cx_strfree()
  */
 #define cx_asprintf(fmt, ...) \
     cx_asprintf_a(cxDefaultAllocator, fmt, __VA_ARGS__)
 
 /**
-* A \c vasprintf like function which allocates space for a string
+* A @c vasprintf like function which allocates space for a string
  * the result is written to.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated,
+ * unless there was an error, in which case the string's pointer
+ * will be @c NULL.
  *
  * @param allocator the CxAllocator used for allocating the string
  * @param fmt format string
@@ -147,6 +157,7 @@
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(2)
+cx_attr_export
 cxmutstr cx_vasprintf_a(
         const CxAllocator *allocator,
         const char *fmt,
@@ -154,65 +165,69 @@
 );
 
 /**
-* A \c vasprintf like function which allocates space for a string
+* A @c vasprintf like function which allocates space for a string
  * the result is written to.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated,
+ * unless there was in error, in which case the string's pointer
+ * will be @c NULL.
  *
- * @param fmt format string
- * @param ap argument list
- * @return the formatted string
+ * @param fmt (@c char*) format string
+ * @param ap (@c va_list) argument list
+ * @return (@c cxmutstr) the formatted string
  * @see cx_asprintf()
  */
 #define cx_vasprintf(fmt, ap) cx_vasprintf_a(cxDefaultAllocator, fmt, ap)
 
 /**
- * A \c printf like function which writes the output to a CxBuffer.
+ * A @c printf like function which writes the output to a CxBuffer.
  *
- * @param buffer a pointer to the buffer the data is written to
- * @param fmt the format string
+ * @param buffer (@c CxBuffer*) a pointer to the buffer the data is written to
+ * @param fmt (@c char*) the format string
  * @param ... additional arguments
- * @return the total number of bytes written
- * @see ucx_fprintf()
+ * @return (@c int) the total number of bytes written or an error code from stdlib printf implementation
+ * @see cx_fprintf()
+ * @see cxBufferWrite()
  */
-#define cx_bprintf(buffer, fmt, ...) cx_fprintf((CxBuffer*)buffer, \
-    (cx_write_func) cxBufferWrite, fmt, __VA_ARGS__)
+#define cx_bprintf(buffer, fmt, ...) cx_fprintf((void*)buffer, \
+    cxBufferWriteFunc, fmt, __VA_ARGS__)
 
 
 /**
- * An \c sprintf like function which reallocates the string when the buffer is not large enough.
+ * An @c sprintf like function which reallocates the string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  *
- * @param str a pointer to the string buffer
- * @param len a pointer to the length of the buffer
- * @param fmt the format string
+ * @param str (@c char**) a pointer to the string buffer
+ * @param len (@c size_t*) a pointer to the length of the buffer
+ * @param fmt (@c char*) the format string
  * @param ... additional arguments
- * @return the length of produced string
+ * @return (@c int) the length of produced string or an error code from stdlib printf implementation
  */
 #define cx_sprintf(str, len, fmt, ...) cx_sprintf_a(cxDefaultAllocator, str, len, fmt, __VA_ARGS__)
 
 /**
- * An \c sprintf like function which reallocates the string when the buffer is not large enough.
+ * An @c sprintf like function which reallocates the string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  *
- * \attention The original buffer MUST have been allocated with the same allocator!
+ * @attention The original buffer MUST have been allocated with the same allocator!
  *
  * @param alloc the allocator to use
  * @param str a pointer to the string buffer
  * @param len a pointer to the length of the buffer
  * @param fmt the format string
  * @param ... additional arguments
- * @return the length of produced string
+ * @return the length of produced string or an error code from stdlib printf implementation
  */
 cx_attr_nonnull_arg(1, 2, 3, 4)
 cx_attr_printf(4, 5)
 cx_attr_cstr_arg(4)
+cx_attr_export
 int cx_sprintf_a(
         CxAllocator *alloc,
         char **str,
@@ -223,38 +238,41 @@
 
 
 /**
- * An \c sprintf like function which reallocates the string when the buffer is not large enough.
+ * An @c sprintf like function which reallocates the string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  *
- * @param str a pointer to the string buffer
- * @param len a pointer to the length of the buffer
- * @param fmt the format string
- * @param ap argument list
- * @return the length of produced string
+ * @param str (@c char**) a pointer to the string buffer
+ * @param len (@c size_t*) a pointer to the length of the buffer
+ * @param fmt (@c char*) the format string
+ * @param ap (@c va_list) argument list
+ * @return (@c int) the length of produced string or an error code from stdlib printf implementation
  */
 #define cx_vsprintf(str, len, fmt, ap) cx_vsprintf_a(cxDefaultAllocator, str, len, fmt, ap)
 
 /**
- * An \c sprintf like function which reallocates the string when the buffer is not large enough.
+ * An @c sprintf like function which reallocates the string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated.
  *
- * \attention The original buffer MUST have been allocated with the same allocator!
+ * @attention The original buffer MUST have been allocated with the same allocator!
  *
  * @param alloc the allocator to use
  * @param str a pointer to the string buffer
  * @param len a pointer to the length of the buffer
  * @param fmt the format string
  * @param ap argument list
- * @return the length of produced string
+ * @return the length of produced string or an error code from stdlib printf implementation
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(4)
+cx_attr_access_rw(2)
+cx_attr_access_rw(3)
+cx_attr_export
 int cx_vsprintf_a(
         CxAllocator *alloc,
         char **str,
@@ -265,52 +283,55 @@
 
 
 /**
- * An \c sprintf like function which allocates a new string when the buffer is not large enough.
+ * An @c sprintf like function which allocates a new string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * The location of the resulting string will \em always be stored to \p str. When the buffer
- * was sufficiently large, \p buf itself will be stored to the location of \p str.
+ * The location of the resulting string will @em always be stored to @p str. When the buffer
+ * was sufficiently large, @p buf itself will be stored to the location of @p str.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  * 
- * \remark When a new string needed to be allocated, the contents of \p buf will be
- * poisoned after the call, because this function tries to produce the string in \p buf, first.
+ * @remark When a new string needed to be allocated, the contents of @p buf will be
+ * poisoned after the call, because this function tries to produce the string in @p buf, first.
  *
- * @param buf a pointer to the buffer
- * @param len a pointer to the length of the buffer
- * @param str a pointer to the location
- * @param fmt the format string
+ * @param buf (@c char*) a pointer to the buffer
+ * @param len (@c size_t*) a pointer to the length of the buffer
+ * @param str (@c char**) a pointer where the location of the result shall be stored
+ * @param fmt (@c char*) the format string
  * @param ... additional arguments
- * @return the length of produced string
+ * @return (@c int) the length of produced string or an error code from stdlib printf implementation
  */
 #define cx_sprintf_s(buf, len, str, fmt, ...) cx_sprintf_sa(cxDefaultAllocator, buf, len, str, fmt, __VA_ARGS__)
 
 /**
- * An \c sprintf like function which allocates a new string when the buffer is not large enough.
+ * An @c sprintf like function which allocates a new string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * The location of the resulting string will \em always be stored to \p str. When the buffer
- * was sufficiently large, \p buf itself will be stored to the location of \p str.
+ * The location of the resulting string will @em always be stored to @p str. When the buffer
+ * was sufficiently large, @p buf itself will be stored to the location of @p str.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string, if successful, is guaranteed to be zero-terminated.
  *
- * \remark When a new string needed to be allocated, the contents of \p buf will be
- * poisoned after the call, because this function tries to produce the string in \p buf, first.
+ * @remark When a new string needed to be allocated, the contents of @p buf will be
+ * poisoned after the call, because this function tries to produce the string in @p buf, first.
  *
  * @param alloc the allocator to use
  * @param buf a pointer to the buffer
  * @param len a pointer to the length of the buffer
- * @param str a pointer to the location
+ * @param str a pointer where the location of the result shall be stored
  * @param fmt the format string
  * @param ... additional arguments
- * @return the length of produced string
+ * @return the length of produced string or an error code from stdlib printf implementation
  */
-__attribute__((__nonnull__(1, 2, 4, 5), __format__(printf, 5, 6)))
 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_attr_export
 int cx_sprintf_sa(
         CxAllocator *alloc,
         char *buf,
@@ -321,50 +342,51 @@
 );
 
 /**
- * An \c sprintf like function which allocates a new string when the buffer is not large enough.
+ * An @c sprintf like function which allocates a new string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * The location of the resulting string will \em always be stored to \p str. When the buffer
- * was sufficiently large, \p buf itself will be stored to the location of \p str.
+ * The location of the resulting string will @em always be stored to @p str. When the buffer
+ * was sufficiently large, @p buf itself will be stored to the location of @p str.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated.
  *
- * \remark When a new string needed to be allocated, the contents of \p buf will be
- * poisoned after the call, because this function tries to produce the string in \p buf, first.
+ * @remark When a new string needed to be allocated, the contents of @p buf will be
+ * poisoned after the call, because this function tries to produce the string in @p buf, first.
  *
- * @param buf a pointer to the buffer
- * @param len a pointer to the length of the buffer
- * @param str a pointer to the location
- * @param fmt the format string
- * @param ap argument list
- * @return the length of produced string
+ * @param buf (@c char*) a pointer to the buffer
+ * @param len (@c size_t*) a pointer to the length of the buffer
+ * @param str (@c char**) a pointer where the location of the result shall be stored
+ * @param fmt (@c char*) the format string
+ * @param ap (@c va_list) argument list
+ * @return (@c int) the length of produced string or an error code from stdlib printf implementation
  */
 #define cx_vsprintf_s(buf, len, str, fmt, ap) cx_vsprintf_sa(cxDefaultAllocator, buf, len, str, fmt, ap)
 
 /**
- * An \c sprintf like function which allocates a new string when the buffer is not large enough.
+ * An @c sprintf like function which allocates a new string when the buffer is not large enough.
  *
- * The size of the buffer will be updated in \p len when necessary.
+ * The size of the buffer will be updated in @p len when necessary.
  *
- * The location of the resulting string will \em always be stored to \p str. When the buffer
- * was sufficiently large, \p buf itself will be stored to the location of \p str.
+ * The location of the resulting string will @em always be stored to @p str. When the buffer
+ * was sufficiently large, @p buf itself will be stored to the location of @p str.
  *
- * \note The resulting string is guaranteed to be zero-terminated.
+ * @note The resulting string is guaranteed to be zero-terminated.
  *
- * \remark When a new string needed to be allocated, the contents of \p buf will be
- * poisoned after the call, because this function tries to produce the string in \p buf, first.
+ * @remark When a new string needed to be allocated, the contents of @p buf will be
+ * poisoned after the call, because this function tries to produce the string in @p buf, first.
  *
  * @param alloc the allocator to use
  * @param buf a pointer to the buffer
  * @param len a pointer to the length of the buffer
- * @param str a pointer to the location
+ * @param str a pointer where the location of the result shall be stored
  * @param fmt the format string
  * @param ap argument list
- * @return the length of produced string
+ * @return the length of produced string or an error code from stdlib printf implementation
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(5)
+cx_attr_export
 int cx_vsprintf_sa(
         CxAllocator *alloc,
         char *buf,
--- a/ucx/cx/properties.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/properties.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file properties.h
- * \brief Interface for parsing data from properties files.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file properties.h
+ * @brief Interface for parsing data from properties files.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_PROPERTIES_H
@@ -59,12 +59,6 @@
     char delimiter;
 
     /**
-     * The character, when appearing at the end of a line, continues that line.
-     * This is '\' by default.
-     */
-    // char continuation; // TODO: line continuation in properties
-
-    /**
      * The first comment character.
      * This is '#' by default.
      */
@@ -81,6 +75,15 @@
      * This is not set by default.
      */
     char comment3;
+
+    /*
+     * The character, when appearing at the end of a line, continues that line.
+     * This is '\' by default.
+     */
+    /**
+     * Reserved for future use.
+     */
+    char continuation;
 };
 
 /**
@@ -91,6 +94,7 @@
 /**
  * Default properties configuration.
  */
+cx_attr_export
 extern const CxPropertiesConfig cx_properties_config_default;
 
 /**
@@ -116,13 +120,13 @@
      * Not used as a status and never returned by any function.
      *
      * You can use this enumerator to check for all "good" status results
-     * by checking if the status is less than \c CX_PROPERTIES_OK.
+     * by checking if the status is less than @c CX_PROPERTIES_OK.
      *
      * A "good" status means, that you can refill data and continue parsing.
      */
     CX_PROPERTIES_OK,
     /**
-     * Input buffer is \c NULL.
+     * Input buffer is @c NULL.
      */
     CX_PROPERTIES_NULL_INPUT,
     /**
@@ -203,7 +207,8 @@
  * @param sink the sink
  * @param key the key
  * @param value the value
- * @return zero on success, non-zero when sinking the k/v-pair failed
+ * @retval zero success
+ * @retval non-zero sinking the k/v-pair failed
  */
 cx_attr_nonnull
 typedef int(*cx_properties_sink_func)(
@@ -241,7 +246,7 @@
  * A function that reads data from a source.
  *
  * When the source is depleted, implementations SHALL provide an empty
- * string in the \p target and return zero.
+ * string in the @p target and return zero.
  * A non-zero return value is only permitted in case of an error.
  *
  * The meaning of the optional parameters is implementation-dependent.
@@ -249,7 +254,8 @@
  * @param prop the properties interface that wants to read from the source
  * @param src the source
  * @param target a string buffer where the read data shall be stored
- * @return zero on success, non-zero when reading data failed
+ * @retval zero success
+ * @retval non-zero reading the data failed
  */
 cx_attr_nonnull
 typedef int(*cx_properties_read_func)(
@@ -263,7 +269,8 @@
  *
  * @param prop the properties interface that wants to read from the source
  * @param src the source
- * @return zero when initialization was successful, non-zero otherwise
+ * @retval zero initialization was successful
+ * @retval non-zero otherwise
  */
 cx_attr_nonnull
 typedef int(*cx_properties_read_init_func)(
@@ -324,12 +331,13 @@
  * @see cxPropertiesInitDefault()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
 
 /**
  * Destroys the properties interface.
  *
- * \note Even when you are certain that you did not use the interface in a
+ * @note Even when you are certain that you did not use the interface in a
  * way that caused a memory allocation, you should call this function anyway.
  * Future versions of the library might add features that need additional memory
  * and you really don't want to search the entire code where you might need
@@ -338,6 +346,7 @@
  * @param prop the properties interface
  */
 cx_attr_nonnull
+cx_attr_export
 void cxPropertiesDestroy(CxProperties *prop);
 
 /**
@@ -358,7 +367,7 @@
 /**
  * Initialize a properties parser with the default configuration.
  *
- * @param prop the properties interface
+ * @param prop (@c CxProperties*) the properties interface
  * @see cxPropertiesInit()
  */
 #define cxPropertiesInitDefault(prop) \
@@ -381,11 +390,13 @@
  * @param prop the properties interface
  * @param buf a pointer to the data
  * @param len the length of the data
- * @return non-zero when a memory allocation was necessary but failed
+ * @retval zero success
+ * @retval non-zero a memory allocation was necessary but failed
  * @see cxPropertiesFill()
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxPropertiesFilln(
         CxProperties *prop,
         const char *buf,
@@ -437,7 +448,8 @@
  *
  * @param prop the properties interface
  * @param str the text to fill in
- * @return non-zero when a memory allocation was necessary but failed
+ * @retval zero success
+ * @retval non-zero a memory allocation was necessary but failed
  * @see cxPropertiesFilln()
  */
 #define cxPropertiesFill(prop, str) _Generic((str), \
@@ -490,6 +502,7 @@
  * @param capacity the capacity of the stack memory
  */
 cx_attr_nonnull
+cx_attr_export
 void cxPropertiesUseStack(
         CxProperties *prop,
         char *buf,
@@ -505,23 +518,30 @@
  * When an incomplete line is encountered, #CX_PROPERTIES_INCOMPLETE_DATA is
  * returned, and you can add more data with #cxPropertiesFill().
  *
- * \remark The incomplete line will be stored in an internal buffer, which is
+ * @remark The incomplete line will be stored in an internal buffer, which is
  * allocated on the heap, by default. If you want to avoid allocations,
  * you can specify sufficient space with cxPropertiesUseStack() after
  * initialization with cxPropertiesInit().
  *
- * \attention The returned strings will point into a buffer that might not be
+ * @attention The returned strings will point into a buffer that might not be
  * available later. It is strongly recommended to copy the strings for further
  * use.
  *
  * @param prop the properties interface
  * @param key a pointer to the cxstring that shall contain the property name
  * @param value a pointer to the cxstring that shall contain the property value
- * @return the status code as defined above
- * @see cxPropertiesFill()
+ * @retval CX_PROPERTIES_NO_ERROR (zero) a key/value pair was found
+ * @retval CX_PROPERTIES_NO_DATA there is no (more) data in the input buffer
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA the data in the input buffer is incomplete
+ * (fill more data and try again)
+ * @retval CX_PROPERTIES_NULL_INPUT the input buffer was never filled
+ * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
+ * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
+ * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxPropertiesStatus cxPropertiesNext(
         CxProperties *prop,
         cxstring *key,
@@ -534,7 +554,7 @@
  * The values stored in the map will be pointers to strings allocated
  * by #cx_strdup_a().
  * The default stdlib allocator will be used, unless you specify a custom
- * allocator in the optional \c data of the sink.
+ * allocator in the optional @c data of the sink.
  *
  * @param map the map that shall consume the k/v-pairs.
  * @return the sink
@@ -542,6 +562,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxPropertiesSink cxPropertiesMapSink(CxMap *map);
 
 /**
@@ -552,6 +573,7 @@
  * @see cxPropertiesLoad()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxPropertiesSource cxPropertiesStringSource(cxstring str);
 
 /**
@@ -565,6 +587,7 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_access_r(1, 2)
+cx_attr_export
 CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len);
 
 /**
@@ -580,6 +603,7 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_cstr_arg(1)
+cx_attr_export
 CxPropertiesSource cxPropertiesCstrSource(const char *str);
 
 /**
@@ -594,6 +618,7 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_access_r(1)
+cx_attr_export
 CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size);
 
 
@@ -605,14 +630,28 @@
  * the return value will be #CX_PROPERTIES_NO_ERROR.
  * When the source was consumed but no k/v-pairs were found, the return value
  * will be #CX_PROPERTIES_NO_DATA.
+ * In case the source data ends unexpectedly, the #CX_PROPERTIES_INCOMPLETE_DATA
+ * is returned. In that case you should call this function again with the same
+ * sink and either an updated source or the same source if the source is able to
+ * yield the missing data.
+ *
  * The other result codes apply, according to their description.
  *
  * @param prop the properties interface
  * @param sink the sink
  * @param source the source
- * @return the status of the last operation
+ * @retval CX_PROPERTIES_NO_ERROR (zero) a key/value pair was found
+ * @retval CX_PROPERTIES_READ_INIT_FAILED initializing the source failed
+ * @retval CX_PROPERTIES_READ_FAILED reading from the source failed
+ * @retval CX_PROPERTIES_SINK_FAILED sinking the properties into the sink failed
+ * @retval CX_PROPERTIES_NO_DATA the source did not provide any key/value pairs
+ * @retval CX_PROPERTIES_INCOMPLETE_DATA the source did not provide enough data
+ * @retval CX_PROPERTIES_INVALID_EMPTY_KEY the properties data contains an illegal empty key
+ * @retval CX_PROPERTIES_INVALID_MISSING_DELIMITER the properties data contains a line without delimiter
+ * @retval CX_PROPERTIES_BUFFER_ALLOC_FAILED an internal allocation was necessary but failed
  */
 cx_attr_nonnull
+cx_attr_export
 CxPropertiesStatus cxPropertiesLoad(
         CxProperties *prop,
         CxPropertiesSink sink,
--- a/ucx/cx/streams.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/streams.h	Tue Feb 25 21:12:11 2025 +0100
@@ -27,13 +27,13 @@
  */
 
 /**
- * \file streams.h
+ * @file streams.h
  *
- * \brief Utility functions for data streams.
+ * @brief Utility functions for data streams.
  *
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_STREAMS_H
@@ -52,12 +52,12 @@
  * @param dest the destination stream
  * @param rfnc the read function
  * @param wfnc the write function
- * @param buf a pointer to the copy buffer or \c NULL if a buffer
+ * @param buf a pointer to the copy buffer or @c NULL if a buffer
  * shall be implicitly created on the heap
- * @param bufsize the size of the copy buffer - if \p buf is \c NULL you can
+ * @param bufsize the size of the copy buffer - if @p buf is @c NULL you can
  * set this to zero to let the implementation decide
  * @param n the maximum number of bytes that shall be copied.
- * If this is larger than \p bufsize, the content is copied over multiple
+ * If this is larger than @p bufsize, the content is copied over multiple
  * iterations.
  * @return the total number of bytes copied
  */
@@ -65,6 +65,7 @@
 cx_attr_access_r(1)
 cx_attr_access_w(2)
 cx_attr_access_w(5)
+cx_attr_export
 size_t cx_stream_bncopy(
         void *src,
         void *dest,
@@ -78,14 +79,14 @@
 /**
  * Reads data from a stream and writes it to another stream.
  *
- * @param src the source stream
- * @param dest the destination stream
- * @param rfnc the read function
- * @param wfnc the write function
- * @param buf a pointer to the copy buffer or \c NULL if a buffer
+ * @param src (@c void*) the source stream
+ * @param dest (@c void*) the destination stream
+ * @param rfnc (@c cx_read_func) the read function
+ * @param wfnc (@c cx_write_func) the write function
+ * @param buf (@c char*) a pointer to the copy buffer or @c NULL if a buffer
  * shall be implicitly created on the heap
- * @param bufsize the size of the copy buffer - if \p buf is \c NULL you can
- * set this to zero to let the implementation decide
+ * @param bufsize (@c size_t) the size of the copy buffer - if @p buf is
+ * @c NULL you can set this to zero to let the implementation decide
  * @return total number of bytes copied
  */
 #define cx_stream_bcopy(src, dest, rfnc, wfnc, buf, bufsize) \
@@ -106,6 +107,7 @@
 cx_attr_nonnull
 cx_attr_access_r(1)
 cx_attr_access_w(2)
+cx_attr_export
 size_t cx_stream_ncopy(
         void *src,
         void *dest,
@@ -119,10 +121,10 @@
  *
  * The data is temporarily stored in a stack allocated buffer.
  *
- * @param src the source stream
- * @param dest the destination stream
- * @param rfnc the read function
- * @param wfnc the write function
+ * @param src (@c void*) the source stream
+ * @param dest (@c void*) the destination stream
+ * @param rfnc (@c cx_read_func) the read function
+ * @param wfnc (@c cx_write_func) the write function
  * @return total number of bytes copied
  */
 #define cx_stream_copy(src, dest, rfnc, wfnc) \
--- a/ucx/cx/string.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/string.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file string.h
- * \brief Strings that know their length.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file string.h
+ * @brief Strings that know their length.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_STRING_H
@@ -42,6 +42,7 @@
 /**
  * The maximum length of the "needle" in cx_strstr() that can use SBO.
  */
+cx_attr_export
 extern const unsigned cx_strstr_sbo_size;
 
 /**
@@ -50,8 +51,7 @@
 struct cx_mutstr_s {
     /**
      * A pointer to the string.
-     * \note The string is not necessarily \c NULL terminated.
-     * Always use the length.
+     * @note The string is not necessarily @c NULL terminated.
      */
     char *ptr;
     /** The length of the string */
@@ -69,8 +69,7 @@
 struct cx_string_s {
     /**
      * A pointer to the immutable string.
-     * \note The string is not necessarily \c NULL terminated.
-     * Always use the length.
+     * @note The string is not necessarily @c NULL terminated.
      */
     const char *ptr;
     /** The length of the string */
@@ -148,7 +147,7 @@
 /**
  * A literal initializer for an UCX string structure.
  *
- * The argument MUST be a string (const char*) \em literal.
+ * The argument MUST be a string (const char*) @em literal.
  *
  * @param literal the string literal
  */
@@ -160,9 +159,9 @@
 /**
  * Wraps a mutable string that must be zero-terminated.
  *
- * The length is implicitly inferred by using a call to \c strlen().
+ * The length is implicitly inferred by using a call to @c strlen().
  *
- * \note the wrapped string will share the specified pointer to the string.
+ * @note the wrapped string will share the specified pointer to the string.
  * If you do want a copy, use cx_strdup() on the return value of this function.
  *
  * If you need to wrap a constant string, use cx_str().
@@ -175,19 +174,20 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_cstr_arg(1)
+cx_attr_export
 cxmutstr cx_mutstr(char *cstring);
 
 /**
  * Wraps a string that does not need to be zero-terminated.
  *
- * The argument may be \c NULL if the length is zero.
+ * The argument may be @c NULL if the length is zero.
  *
- * \note the wrapped string will share the specified pointer to the string.
+ * @note the wrapped string will share the specified pointer to the string.
  * If you do want a copy, use cx_strdup() on the return value of this function.
  *
  * If you need to wrap a constant string, use cx_strn().
  *
- * @param cstring  the string to wrap (or \c NULL, only if the length is zero)
+ * @param cstring  the string to wrap (or @c NULL, only if the length is zero)
  * @param length   the length of the string
  * @return the wrapped string
  *
@@ -195,6 +195,7 @@
  */
 cx_attr_nodiscard
 cx_attr_access_rw(1, 2)
+cx_attr_export
 cxmutstr cx_mutstrn(
         char *cstring,
         size_t length
@@ -203,9 +204,9 @@
 /**
  * Wraps a string that must be zero-terminated.
  *
- * The length is implicitly inferred by using a call to \c strlen().
+ * The length is implicitly inferred by using a call to @c strlen().
  *
- * \note the wrapped string will share the specified pointer to the string.
+ * @note the wrapped string will share the specified pointer to the string.
  * If you do want a copy, use cx_strdup() on the return value of this function.
  *
  * If you need to wrap a non-constant string, use cx_mutstr().
@@ -218,20 +219,21 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_cstr_arg(1)
+cx_attr_export
 cxstring cx_str(const char *cstring);
 
 
 /**
  * Wraps a string that does not need to be zero-terminated.
  *
- * The argument may be \c NULL if the length is zero.
+ * The argument may be @c NULL if the length is zero.
  *
- * \note the wrapped string will share the specified pointer to the string.
+ * @note the wrapped string will share the specified pointer to the string.
  * If you do want a copy, use cx_strdup() on the return value of this function.
  *
  * If you need to wrap a non-constant string, use cx_mutstrn().
  *
- * @param cstring  the string to wrap (or \c NULL, only if the length is zero)
+ * @param cstring  the string to wrap (or @c NULL, only if the length is zero)
  * @param length   the length of the string
  * @return the wrapped string
  *
@@ -239,6 +241,7 @@
  */
 cx_attr_nodiscard
 cx_attr_access_r(1, 2)
+cx_attr_export
 cxstring cx_strn(
         const char *cstring,
         size_t length
@@ -282,12 +285,12 @@
 *
 * Does nothing for already immutable strings.
 *
-* \note This is not seriously a cast. Instead, you get a copy
+* @note This is not seriously a cast. Instead, you get a copy
 * of the struct with the desired pointer type. Both structs still
 * point to the same location, though!
 *
-* @param str the mutable string to cast
-* @return an immutable copy of the string pointer
+* @param str (@c cxstring or @c cxmutstr) the string to cast
+* @return (@c cxstring) an immutable copy of the string pointer
 */
 #define cx_strcast(str) _Generic((str), \
         cxmutstr: cx_strcast_m, \
@@ -296,24 +299,27 @@
 #endif
 
 /**
- * Passes the pointer in this string to \c free().
+ * Passes the pointer in this string to @c free().
  *
- * The pointer in the struct is set to \c NULL and the length is set to zero.
+ * The pointer in the struct is set to @c NULL and the length is set to zero
+ * which means that this function protects you against double-free.
  *
- * \note There is no implementation for cxstring, because it is unlikely that
+ * @note There is no implementation for cxstring, because it is unlikely that
  * you ever have a <code>const char*</code> you are really supposed to free.
  * If you encounter such situation, you should double-check your code.
  *
  * @param str the string to free
  */
+cx_attr_export
 void cx_strfree(cxmutstr *str);
 
 /**
  * Passes the pointer in this string to the allocators free function.
  *
- * The pointer in the struct is set to \c NULL and the length is set to zero.
+ * The pointer in the struct is set to @c NULL and the length is set to zero
+ * which means that this function protects you against double-free.
  *
- * \note There is no implementation for cxstring, because it is unlikely that
+ * @note There is no implementation for cxstring, because it is unlikely that
  * you ever have a <code>const char*</code> you are really supposed to free.
  * If you encounter such situation, you should double-check your code.
  *
@@ -321,6 +327,7 @@
  * @param str the string to free
  */
 cx_attr_nonnull_arg(1)
+cx_attr_export
 void cx_strfree_a(
         const CxAllocator *alloc,
         cxmutstr *str
@@ -331,7 +338,7 @@
  * 
  * If this sum overflows, errno is set to EOVERFLOW.
  *
- * \attention if the count argument is larger than the number of the
+ * @attention if the count argument is larger than the number of the
  * specified strings, the behavior is undefined.
  *
  * @param count    the total number of specified strings
@@ -339,6 +346,7 @@
  * @return the accumulated length of all strings
  */
 cx_attr_nodiscard
+cx_attr_export
 size_t cx_strlen(
         size_t count,
         ...
@@ -348,26 +356,27 @@
  * Concatenates strings.
  *
  * The resulting string will be allocated by the specified allocator.
- * So developers \em must pass the return value to cx_strfree_a() eventually.
+ * So developers @em must pass the return value to cx_strfree_a() eventually.
  *
- * If \p str already contains a string, the memory will be reallocated and
+ * If @p str already contains a string, the memory will be reallocated and
  * the other strings are appended. Otherwise, new memory is allocated.
  *
  * If memory allocation fails, the pointer in the returned string will
- * be \c NULL. Depending on the allocator, \c errno might be set.
+ * be @c NULL. Depending on the allocator, @c errno might be set.
  *
- * \note It is guaranteed that there is only one allocation for the
+ * @note It is guaranteed that there is only one allocation for the
  * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
  * @param alloc the allocator to use
  * @param str   the string the other strings shall be concatenated to
  * @param count the number of the other following strings to concatenate
- * @param ...   all other strings
+ * @param ...   all other UCX strings
  * @return the concatenated string
  */
 cx_attr_nodiscard
 cx_attr_nonnull
+cx_attr_export
 cxmutstr cx_strcat_ma(
         const CxAllocator *alloc,
         cxmutstr str,
@@ -379,19 +388,19 @@
  * Concatenates strings and returns a new string.
  *
  * The resulting string will be allocated by the specified allocator.
- * So developers \em must pass the return value to cx_strfree_a() eventually.
+ * So developers @em must pass the return value to cx_strfree_a() eventually.
  *
 * If memory allocation fails, the pointer in the returned string will
- * be \c NULL. Depending on the allocator, \c errno might be set.
+ * be @c NULL. Depending on the allocator, @c errno might be set.
  *
- * \note It is guaranteed that there is only one allocation for the
+ * @note It is guaranteed that there is only one allocation for the
  * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
- * @param alloc the allocator to use
- * @param count the number of the other following strings to concatenate
- * @param ...   all other strings
- * @return the concatenated string
+ * @param alloc (@c CxAllocator*) the allocator to use
+ * @param count (@c size_t) the number of the other following strings to concatenate
+ * @param ...   all other UCX strings
+ * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat_a(alloc, count, ...) \
 cx_strcat_ma(alloc, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
@@ -399,19 +408,19 @@
 /**
  * Concatenates strings and returns a new string.
  *
- * The resulting string will be allocated by standard \c malloc().
- * So developers \em must pass the return value to cx_strfree() eventually.
+ * The resulting string will be allocated by standard @c malloc().
+ * So developers @em must pass the return value to cx_strfree() eventually.
  *
 * If memory allocation fails, the pointer in the returned string will
- * be \c NULL and \c errno might be set.
+ * be @c NULL and @c errno might be set.
  *
- * \note It is guaranteed that there is only one allocation for the
+ * @note It is guaranteed that there is only one allocation for the
  * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
- * @param count   the number of the other following strings to concatenate
- * @param ...     all other strings
- * @return the concatenated string
+ * @param count (@c size_t) the number of the other following strings to concatenate
+ * @param ... all other UCX strings
+ * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat(count, ...) \
 cx_strcat_ma(cxDefaultAllocator, cx_mutstrn(NULL, 0), count, __VA_ARGS__)
@@ -419,23 +428,23 @@
 /**
  * Concatenates strings.
  *
- * The resulting string will be allocated by standard \c malloc().
- * So developers \em must pass the return value to cx_strfree() eventually.
+ * The resulting string will be allocated by standard @c malloc().
+ * So developers @em must pass the return value to cx_strfree() eventually.
  *
- * If \p str already contains a string, the memory will be reallocated and
+ * If @p str already contains a string, the memory will be reallocated and
  * the other strings are appended. Otherwise, new memory is allocated.
  *
 * If memory allocation fails, the pointer in the returned string will
- * be \c NULL and \c errno might be set.
+ * be @c NULL and @c errno might be set.
  *
- * \note It is guaranteed that there is only one allocation for the
+ * @note It is guaranteed that there is only one allocation for the
  * resulting string.
  * It is also guaranteed that the returned string is zero-terminated.
  *
- * @param str     the string the other strings shall be concatenated to
- * @param count   the number of the other following strings to concatenate
- * @param ...     all other strings
- * @return the concatenated string
+ * @param str (@c cxmutstr) the string the other strings shall be concatenated to
+ * @param count (@c size_t) the number of the other following strings to concatenate
+ * @param ... all other strings
+ * @return (@c cxmutstr) the concatenated string
  */
 #define cx_strcat_m(str, count, ...) \
 cx_strcat_ma(cxDefaultAllocator, str, count, __VA_ARGS__)
@@ -443,19 +452,20 @@
 /**
  * Returns a substring starting at the specified location.
  *
- * \attention the new string references the same memory area as the
- * input string and is usually \em not zero-terminated.
+ * @attention the new string references the same memory area as the
+ * input string and is usually @em not zero-terminated.
  * Use cx_strdup() to get a copy.
  *
  * @param string input string
  * @param start  start location of the substring
- * @return a substring of \p string starting at \p start
+ * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubsl()
  * @see cx_strsubs_m()
  * @see cx_strsubsl_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strsubs(
         cxstring string,
         size_t start
@@ -464,23 +474,24 @@
 /**
  * Returns a substring starting at the specified location.
  *
- * The returned string will be limited to \p length bytes or the number
- * of bytes available in \p string, whichever is smaller.
+ * The returned string will be limited to @p length bytes or the number
+ * of bytes available in @p string, whichever is smaller.
  *
- * \attention the new string references the same memory area as the
- * input string and is usually \em not zero-terminated.
+ * @attention the new string references the same memory area as the
+ * input string and is usually @em not zero-terminated.
  * Use cx_strdup() to get a copy.
  *
  * @param string input string
  * @param start  start location of the substring
  * @param length the maximum length of the returned string
- * @return a substring of \p string starting at \p start
+ * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubs()
  * @see cx_strsubs_m()
  * @see cx_strsubsl_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strsubsl(
         cxstring string,
         size_t start,
@@ -490,19 +501,20 @@
 /**
  * Returns a substring starting at the specified location.
  *
- * \attention the new string references the same memory area as the
- * input string and is usually \em not zero-terminated.
+ * @attention the new string references the same memory area as the
+ * input string and is usually @em not zero-terminated.
  * Use cx_strdup() to get a copy.
  *
  * @param string input string
  * @param start  start location of the substring
- * @return a substring of \p string starting at \p start
+ * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubsl_m()
  * @see cx_strsubs()
  * @see cx_strsubsl()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strsubs_m(
         cxmutstr string,
         size_t start
@@ -511,23 +523,24 @@
 /**
  * Returns a substring starting at the specified location.
  *
- * The returned string will be limited to \p length bytes or the number
- * of bytes available in \p string, whichever is smaller.
+ * The returned string will be limited to @p length bytes or the number
+ * of bytes available in @p string, whichever is smaller.
  *
- * \attention the new string references the same memory area as the
- * input string and is usually \em not zero-terminated.
+ * @attention the new string references the same memory area as the
+ * input string and is usually @em not zero-terminated.
  * Use cx_strdup() to get a copy.
  *
  * @param string input string
  * @param start  start location of the substring
  * @param length the maximum length of the returned string
- * @return a substring of \p string starting at \p start
+ * @return a substring of @p string starting at @p start
  *
  * @see cx_strsubs_m()
  * @see cx_strsubs()
  * @see cx_strsubsl()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strsubsl_m(
         cxmutstr string,
         size_t start,
@@ -542,11 +555,12 @@
  *
  * @param string the string where to locate the character
  * @param chr    the character to locate
- * @return       a substring starting at the first location of \p chr
+ * @return       a substring starting at the first location of @p chr
  *
  * @see cx_strchr_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strchr(
         cxstring string,
         int chr
@@ -560,11 +574,12 @@
  *
  * @param string the string where to locate the character
  * @param chr    the character to locate
- * @return       a substring starting at the first location of \p chr
+ * @return       a substring starting at the first location of @p chr
  *
  * @see cx_strchr()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strchr_m(
         cxmutstr string,
         int chr
@@ -578,11 +593,12 @@
  *
  * @param string the string where to locate the character
  * @param chr    the character to locate
- * @return       a substring starting at the last location of \p chr
+ * @return       a substring starting at the last location of @p chr
  *
  * @see cx_strrchr_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strrchr(
         cxstring string,
         int chr
@@ -596,11 +612,12 @@
  *
  * @param string the string where to locate the character
  * @param chr    the character to locate
- * @return       a substring starting at the last location of \p chr
+ * @return       a substring starting at the last location of @p chr
  *
  * @see cx_strrchr()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strrchr_m(
         cxmutstr string,
         int chr
@@ -610,19 +627,20 @@
  * Returns a substring starting at the location of the first occurrence of the
  * specified string.
  *
- * If \p haystack does not contain \p needle, an empty string is returned.
+ * If @p haystack does not contain @p needle, an empty string is returned.
  *
- * If \p needle is an empty string, the complete \p haystack is
+ * If @p needle is an empty string, the complete @p haystack is
  * returned.
  *
  * @param haystack the string to be scanned
  * @param needle  string containing the sequence of characters to match
  * @return       a substring starting at the first occurrence of
- *               \p needle, or an empty string, if the sequence is not
+ *               @p needle, or an empty string, if the sequence is not
  *               contained
  * @see cx_strstr_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strstr(
         cxstring haystack,
         cxstring needle
@@ -632,19 +650,20 @@
  * Returns a substring starting at the location of the first occurrence of the
  * specified string.
  *
- * If \p haystack does not contain \p needle, an empty string is returned.
+ * If @p haystack does not contain @p needle, an empty string is returned.
  *
- * If \p needle is an empty string, the complete \p haystack is
+ * If @p needle is an empty string, the complete @p haystack is
  * returned.
  *
  * @param haystack the string to be scanned
  * @param needle  string containing the sequence of characters to match
  * @return       a substring starting at the first occurrence of
- *               \p needle, or an empty string, if the sequence is not
+ *               @p needle, or an empty string, if the sequence is not
  *               contained
  * @see cx_strstr()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strstr_m(
         cxmutstr haystack,
         cxstring needle
@@ -653,18 +672,19 @@
 /**
  * Splits a given string using a delimiter string.
  *
- * \note The resulting array contains strings that point to the source
- * \p string. Use cx_strdup() to get copies.
+ * @note The resulting array contains strings that point to the source
+ * @p string. Use cx_strdup() to get copies.
  *
  * @param string the string to split
  * @param delim  the delimiter
  * @param limit the maximum number of split items
- * @param output a pre-allocated array of at least \p limit length
+ * @param output a 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_attr_export
 size_t cx_strsplit(
         cxstring string,
         cxstring delim,
@@ -675,13 +695,13 @@
 /**
  * Splits a given string using a delimiter string.
  *
- * The array pointed to by \p output will be allocated by \p allocator.
+ * The array pointed to by @p output will be allocated by @p allocator.
  *
- * \note The resulting array contains strings that point to the source
- * \p string. Use cx_strdup() to get copies.
+ * @note The resulting array contains strings that point to the source
+ * @p string. Use cx_strdup() to get copies.
  *
- * \attention If allocation fails, the \c NULL pointer will be written to
- * \p output and the number returned will be zero.
+ * @attention If allocation fails, the @c NULL pointer will be written to
+ * @p output and the number returned will be zero.
  *
  * @param allocator the allocator to use for allocating the resulting array
  * @param string the string to split
@@ -694,6 +714,7 @@
 cx_attr_nodiscard
 cx_attr_nonnull
 cx_attr_access_w(5)
+cx_attr_export
 size_t cx_strsplit_a(
         const CxAllocator *allocator,
         cxstring string,
@@ -706,18 +727,19 @@
 /**
  * Splits a given string using a delimiter string.
  *
- * \note The resulting array contains strings that point to the source
- * \p string. Use cx_strdup() to get copies.
+ * @note The resulting array contains strings that point to the source
+ * @p string. Use cx_strdup() to get copies.
  *
  * @param string the string to split
  * @param delim  the delimiter
  * @param limit the maximum number of split items
- * @param output a pre-allocated array of at least \p limit length
+ * @param output a 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_attr_export
 size_t cx_strsplit_m(
         cxmutstr string,
         cxstring delim,
@@ -728,13 +750,13 @@
 /**
  * Splits a given string using a delimiter string.
  *
- * The array pointed to by \p output will be allocated by \p allocator.
+ * The array pointed to by @p output will be allocated by @p allocator.
  *
- * \note The resulting array contains strings that point to the source
- * \p string. Use cx_strdup() to get copies.
+ * @note The resulting array contains strings that point to the source
+ * @p string. Use cx_strdup() to get copies.
  *
- * \attention If allocation fails, the \c NULL pointer will be written to
- * \p output and the number returned will be zero.
+ * @attention If allocation fails, the @c NULL pointer will be written to
+ * @p output and the number returned will be zero.
  *
  * @param allocator the allocator to use for allocating the resulting array
  * @param string the string to split
@@ -747,6 +769,7 @@
 cx_attr_nodiscard
 cx_attr_nonnull
 cx_attr_access_w(5)
+cx_attr_export
 size_t cx_strsplit_ma(
         const CxAllocator *allocator,
         cxmutstr string,
@@ -760,10 +783,11 @@
  *
  * @param s1 the first string
  * @param s2 the second string
- * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
- * than \p s2, zero if both strings equal
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_strcmp(
         cxstring s1,
         cxstring s2
@@ -774,10 +798,11 @@
  *
  * @param s1 the first string
  * @param s2 the second string
- * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
- * than \p s2, zero if both strings equal ignoring case
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal ignoring case
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_strcasecmp(
         cxstring s1,
         cxstring s2
@@ -790,11 +815,12 @@
  *
  * @param s1 the first string
  * @param s2 the second string
- * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
- * than \p s2, zero if both strings equal
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal
  */
 cx_attr_nodiscard
 cx_attr_nonnull
+cx_attr_export
 int cx_strcmp_p(
         const void *s1,
         const void *s2
@@ -807,11 +833,12 @@
  *
  * @param s1 the first string
  * @param s2 the second string
- * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
- * than \p s2, zero if both strings equal ignoring case
+ * @return negative if @p s1 is smaller than @p s2, positive if @p s1 is larger
+ * than @p s2, zero if both strings equal ignoring case
  */
 cx_attr_nodiscard
 cx_attr_nonnull
+cx_attr_export
 int cx_strcasecmp_p(
         const void *s1,
         const void *s2
@@ -821,9 +848,9 @@
 /**
  * Creates a duplicate of the specified string.
  *
- * The new string will contain a copy allocated by \p allocator.
+ * The new string will contain a copy allocated by @p allocator.
  *
- * \note The returned string is guaranteed to be zero-terminated.
+ * @note The returned string is guaranteed to be zero-terminated.
  *
  * @param allocator the allocator to use
  * @param string the string to duplicate
@@ -832,7 +859,8 @@
  */
 cx_attr_nodiscard
 cx_attr_nonnull
-cxmutstr cx_strdup_a(
+cx_attr_export
+cxmutstr cx_strdup_a_(
         const CxAllocator *allocator,
         cxstring string
 );
@@ -840,68 +868,58 @@
 /**
  * Creates a duplicate of the specified string.
  *
- * The new string will contain a copy allocated by standard
- * \c malloc(). So developers \em must pass the return value to cx_strfree().
+ * The new string will contain a copy allocated by @p allocator.
+ *
+ * @note The returned string is guaranteed to be zero-terminated.
  *
- * \note The returned string is guaranteed to be zero-terminated.
- *
+ * @param allocator (@c CxAllocator*) the allocator to use
  * @param string the string to duplicate
- * @return a duplicate of the string
- * @see cx_strdup_a()
+ * @return (@c cxmutstr) a duplicate of the string
+ * @see cx_strdup()
+ * @see cx_strfree_a()
  */
-#define cx_strdup(string) cx_strdup_a(cxDefaultAllocator, string)
-
-
-/**
- * Creates a duplicate of the specified string.
- *
- * The new string will contain a copy allocated by \p allocator.
- *
- * \note The returned string is guaranteed to be zero-terminated.
- *
- * @param allocator the allocator to use
- * @param string the string to duplicate
- * @return a duplicate of the string
- * @see cx_strdup_m()
- */
-#define cx_strdup_ma(allocator, string) cx_strdup_a(allocator, cx_strcast(string))
+#define cx_strdup_a(allocator, string) \
+    cx_strdup_a_((allocator), cx_strcast((string)))
 
 /**
  * Creates a duplicate of the specified string.
  *
  * The new string will contain a copy allocated by standard
- * \c malloc(). So developers \em must pass the return value to cx_strfree().
+ * @c malloc(). So developers @em must pass the return value to cx_strfree().
  *
- * \note The returned string is guaranteed to be zero-terminated.
+ * @note The returned string is guaranteed to be zero-terminated.
  *
  * @param string the string to duplicate
- * @return a duplicate of the string
- * @see cx_strdup_ma()
+ * @return (@c cxmutstr) a duplicate of the string
+ * @see cx_strdup_a()
+ * @see cx_strfree()
  */
-#define cx_strdup_m(string) cx_strdup_a(cxDefaultAllocator, cx_strcast(string))
+#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.
+ * @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
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strtrim(cxstring string);
 
 /**
  * Omits leading and trailing spaces.
  *
- * \note the returned string references the same memory, thus you
- * must \em not free the returned memory.
+ * @note the returned string references the same memory, thus you
+ * must @em not free the returned memory.
  *
  * @param string the string that shall be trimmed
  * @return the trimmed string
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strtrim_m(cxmutstr string);
 
 /**
@@ -909,10 +927,11 @@
  *
  * @param string the string to check
  * @param prefix the prefix the string should have
- * @return \c true, if and only if the string has the specified prefix,
- * \c false otherwise
+ * @return @c true, if and only if the string has the specified prefix,
+ * @c false otherwise
  */
 cx_attr_nodiscard
+cx_attr_export
 bool cx_strprefix(
         cxstring string,
         cxstring prefix
@@ -923,10 +942,11 @@
  *
  * @param string the string to check
  * @param suffix the suffix the string should have
- * @return \c true, if and only if the string has the specified suffix,
- * \c false otherwise
+ * @return @c true, if and only if the string has the specified suffix,
+ * @c false otherwise
  */
 cx_attr_nodiscard
+cx_attr_export
 bool cx_strsuffix(
         cxstring string,
         cxstring suffix
@@ -937,10 +957,11 @@
  *
  * @param string the string to check
  * @param prefix the prefix the string should have
- * @return \c true, if and only if the string has the specified prefix,
- * \c false otherwise
+ * @return @c true, if and only if the string has the specified prefix,
+ * @c false otherwise
  */
 cx_attr_nodiscard
+cx_attr_export
 bool cx_strcaseprefix(
         cxstring string,
         cxstring prefix
@@ -951,42 +972,22 @@
  *
  * @param string the string to check
  * @param suffix the suffix the string should have
- * @return \c true, if and only if the string has the specified suffix,
- * \c false otherwise
+ * @return @c true, if and only if the string has the specified suffix,
+ * @c false otherwise
  */
 cx_attr_nodiscard
+cx_attr_export
 bool cx_strcasesuffix(
         cxstring string,
         cxstring suffix
 );
 
 /**
- * Converts the string to lower case.
- *
- * The change is made in-place. If you want a copy, use cx_strdup(), first.
- *
- * @param string the string to modify
- * @see cx_strdup()
- */
-void cx_strlower(cxmutstr string);
-
-/**
- * Converts the string to upper case.
+ * Replaces a string with another string.
  *
- * The change is made in-place. If you want a copy, use cx_strdup(), first.
+ * Replaces at most @p replmax occurrences.
  *
- * @param string the string to modify
- * @see cx_strdup()
- */
-void cx_strupper(cxmutstr string);
-
-/**
- * Replaces a pattern in a string with another string.
- *
- * The pattern is taken literally and is no regular expression.
- * Replaces at most \p replmax occurrences.
- *
- * The returned string will be allocated by \p allocator and is guaranteed
+ * The returned string will be allocated by @p allocator and is guaranteed
  * to be zero-terminated.
  *
  * If allocation fails, or the input string is empty,
@@ -994,81 +995,76 @@
  *
  * @param allocator the allocator to use
  * @param str the string where replacements should be applied
- * @param pattern the pattern to search for
+ * @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
  */
 cx_attr_nodiscard
 cx_attr_nonnull
+cx_attr_export
 cxmutstr cx_strreplacen_a(
         const CxAllocator *allocator,
         cxstring str,
-        cxstring pattern,
+        cxstring search,
         cxstring replacement,
         size_t replmax
 );
 
 /**
- * Replaces a pattern in a string with another string.
+ * Replaces a string with another string.
  *
- * The pattern is taken literally and is no regular expression.
- * Replaces at most \p replmax occurrences.
+ * Replaces at most @p replmax occurrences.
  *
- * The returned string will be allocated by \c malloc() and is guaranteed
+ * The returned string will be allocated by @c malloc() and is guaranteed
  * to be zero-terminated.
  *
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param str the string where replacements should be applied
- * @param pattern the pattern to search for
- * @param replacement the replacement string
- * @param replmax maximum number of replacements
- * @return the resulting string after applying the replacements
+ * @param str (@c cxstring) the string where replacements should be applied
+ * @param search (@c cxstring) the string to search for
+ * @param replacement (@c cxstring) the replacement string
+ * @param replmax (@c size_t) maximum number of replacements
+ * @return (@c cxmutstr) the resulting string after applying the replacements
  */
-#define cx_strreplacen(str, pattern, replacement, replmax) \
-cx_strreplacen_a(cxDefaultAllocator, str, pattern, replacement, replmax)
+#define cx_strreplacen(str, search, replacement, replmax) \
+cx_strreplacen_a(cxDefaultAllocator, str, search, replacement, replmax)
 
 /**
- * Replaces a pattern in a string with another string.
+ * Replaces a string with another string.
  *
- * The pattern is taken literally and is no regular expression.
- *
- * The returned string will be allocated by \p allocator and is guaranteed
+ * The returned string will be allocated by @p allocator and is guaranteed
  * to be zero-terminated.
  *
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param allocator the allocator to use
- * @param str the string where replacements should be applied
- * @param pattern the pattern to search for
- * @param replacement the replacement string
- * @return the resulting string after applying the replacements
+ * @param allocator (@c CxAllocator*) the allocator to use
+ * @param str (@c cxstring) the string where replacements should be applied
+ * @param search (@c cxstring) the string to search for
+ * @param replacement (@c cxstring) the replacement string
+ * @return (@c cxmutstr) the resulting string after applying the replacements
  */
-#define cx_strreplace_a(allocator, str, pattern, replacement) \
-cx_strreplacen_a(allocator, str, pattern, replacement, SIZE_MAX)
+#define cx_strreplace_a(allocator, str, search, replacement) \
+cx_strreplacen_a(allocator, str, search, replacement, SIZE_MAX)
 
 /**
- * Replaces a pattern in a string with another string.
+ * Replaces a string with another string.
  *
- * The pattern is taken literally and is no regular expression.
- * Replaces at most \p replmax occurrences.
- *
- * The returned string will be allocated by \c malloc() and is guaranteed
+ * The returned string will be allocated by @c malloc() and is guaranteed
  * to be zero-terminated.
  *
  * If allocation fails, or the input string is empty,
  * the returned string will be empty.
  *
- * @param str the string where replacements should be applied
- * @param pattern the pattern to search for
- * @param replacement the replacement string
- * @return the resulting string after applying the replacements
+ * @param str (@c cxstring) the string where replacements should be applied
+ * @param search (@c cxstring) the string to search for
+ * @param replacement (@c cxstring) the replacement string
+ * @return (@c cxmutstr) the resulting string after applying the replacements
  */
-#define cx_strreplace(str, pattern, replacement) \
-cx_strreplacen_a(cxDefaultAllocator, str, pattern, replacement, SIZE_MAX)
+#define cx_strreplace(str, search, replacement) \
+cx_strreplacen_a(cxDefaultAllocator, str, search, replacement, SIZE_MAX)
 
 /**
  * Creates a string tokenization context.
@@ -1079,26 +1075,23 @@
  * @return a new string tokenization context
  */
 cx_attr_nodiscard
-CxStrtokCtx cx_strtok(
+cx_attr_export
+CxStrtokCtx cx_strtok_(
         cxstring str,
         cxstring delim,
         size_t limit
 );
 
 /**
-* Creates a string tokenization context for a mutable string.
-*
-* @param str the string to tokenize
-* @param delim the delimiter (must not be empty)
-* @param limit the maximum number of tokens that shall be returned
-* @return a new string tokenization context
-*/
-cx_attr_nodiscard
-CxStrtokCtx cx_strtok_m(
-        cxmutstr str,
-        cxstring delim,
-        size_t limit
-);
+ * Creates a string tokenization context.
+ *
+ * @param str the string to tokenize
+ * @param delim the delimiter string (must not be empty)
+ * @param limit (@c size_t) the maximum number of tokens that shall be returned
+ * @return (@c CxStrtokCtx) a new string tokenization context
+ */
+#define cx_strtok(str, delim, limit) \
+    cx_strtok_(cx_strcast((str)), cx_strcast((delim)), (limit))
 
 /**
  * Returns the next token.
@@ -1113,6 +1106,7 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_access_w(2)
+cx_attr_export
 bool cx_strtok_next(
         CxStrtokCtx *ctx,
         cxstring *token
@@ -1122,6 +1116,8 @@
  * Returns the next token of a mutable string.
  *
  * 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.
  *
@@ -1133,6 +1129,7 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_access_w(2)
+cx_attr_export
 bool cx_strtok_next_m(
         CxStrtokCtx *ctx,
         cxmutstr *token
@@ -1147,6 +1144,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 void cx_strtok_delim(
         CxStrtokCtx *ctx,
         const cxstring *delim,
@@ -1158,90 +1156,276 @@
  * ------------------------------------------------------------------------- */
 
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtos_lc(cxstring str, short *output, int base, const char *groupsep);
-/**
- * \copydoc cx_strtouz_lc()
- */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoi_lc(cxstring str, int *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtos_lc_(cxstring str, short *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtol_lc(cxstring str, long *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoi_lc_(cxstring str, int *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoll_lc(cxstring str, long long *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtol_lc_(cxstring str, long *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoi8_lc(cxstring str, int8_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoll_lc_(cxstring str, long long *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoi16_lc(cxstring str, int16_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoi8_lc_(cxstring str, int8_t *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoi32_lc(cxstring str, int32_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoi16_lc_(cxstring str, int16_t *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoi64_lc(cxstring str, int64_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoi32_lc_(cxstring str, int32_t *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoz_lc(cxstring str, ssize_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoi64_lc_(cxstring str, int64_t *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtous_lc(cxstring str, unsigned short *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtous_lc_(cxstring str, unsigned short *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtou_lc(cxstring str, unsigned int *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtou_lc_(cxstring str, unsigned int *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoul_lc(cxstring str, unsigned long *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoul_lc_(cxstring str, unsigned long *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtoull_lc(cxstring str, unsigned long long *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoull_lc_(cxstring str, unsigned long long *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtou8_lc(cxstring str, uint8_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtou8_lc_(cxstring str, uint8_t *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtou16_lc(cxstring str, uint16_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtou16_lc_(cxstring str, uint16_t *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtou32_lc(cxstring str, uint32_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtou32_lc_(cxstring str, uint32_t *output, int base, const char *groupsep);
+
 /**
- * \copydoc cx_strtouz_lc()
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtou64_lc(cxstring str, uint64_t *output, int base, const char *groupsep);
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtou64_lc_(cxstring str, uint64_t *output, int base, const char *groupsep);
 
 /**
  * Converts a string to a number.
@@ -1254,10 +1438,654 @@
  * @param output a pointer to the integer variable where the result shall be stored
  * @param base 2, 8, 10, or 16
  * @param groupsep each character in this string is treated as group separator and ignored during conversion
- * @return zero on success, non-zero if conversion was not possible
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtoz_lc_(cxstring str, size_t *output, int base, const char *groupsep);
+
+/**
+ * Converts a string to a single precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the float variable where the result shall be stored
+ * @param decsep the decimal separator
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtof_lc_(cxstring str, float *output, char decsep, const char *groupsep);
+
+/**
+ * Converts a string to a double precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the float variable where the result shall be stored
+ * @param decsep the decimal separator
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+cx_attr_access_w(2) cx_attr_nonnull_arg(2) cx_attr_export
+int cx_strtod_lc_(cxstring str, double *output, char decsep, const char *groupsep);
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtos_lc(str, output, base, groupsep) cx_strtos_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi_lc(str, output, base, groupsep) cx_strtoi_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtol_lc(str, output, base, groupsep) cx_strtol_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoll_lc(str, output, base, groupsep) cx_strtoll_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi8_lc(str, output, base, groupsep) cx_strtoi8_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi16_lc(str, output, base, groupsep) cx_strtoi16_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi32_lc(str, output, base, groupsep) cx_strtoi32_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi64_lc(str, output, base, groupsep) cx_strtoi64_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtous_lc(str, output, base, groupsep) cx_strtous_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou_lc(str, output, base, groupsep) cx_strtou_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoul_lc(str, output, base, groupsep) cx_strtoul_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoull_lc(str, output, base, groupsep) cx_strtoull_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou8_lc(str, output, base, groupsep) cx_strtou8_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou16_lc(str, output, base, groupsep) cx_strtou16_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou32_lc(str, output, base, groupsep) cx_strtou32_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou64_lc(str, output, base, groupsep) cx_strtou64_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @param groupsep (@c const @c char*) each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtouz_lc(cxstring str, size_t *output, int base, const char *groupsep);
+#define cx_strtoz_lc(str, output, base, groupsep) cx_strtoz_lc_(cx_strcast(str), output, base, groupsep)
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtos(str, output, base) cx_strtos_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi(str, output, base) cx_strtoi_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtol(str, output, base) cx_strtol_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoll(str, output, base) cx_strtoll_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi8(str, output, base) cx_strtoi8_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi16(str, output, base) cx_strtoi16_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi32(str, output, base) cx_strtoi32_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoi64(str, output, base) cx_strtoi64_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoz(str, output, base) cx_strtoz_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtous(str, output, base) cx_strtous_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou(str, output, base) cx_strtou_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoul(str, output, base) cx_strtoul_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtoull(str, output, base) cx_strtoull_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou8(str, output, base) cx_strtou8_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou16(str, output, base) cx_strtou16_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou32(str, output, base) cx_strtou32_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
+ * It sets errno to ERANGE when the target datatype is too small.
+ *
+ * The comma character is treated as group separator and ignored during parsing.
+ * If you want to choose the set of group separators, use the @c _lc variant of this function (e.g. cx_strtoz_lc()).
+ *
+ * @param str the string to convert
+ * @param output a pointer to the integer variable where the result shall be stored
+ * @param base 2, 8, 10, or 16
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtou64(str, output, base) cx_strtou64_lc_(cx_strcast(str), output, base, ",")
+
+/**
+ * Converts a string to a single precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the float variable where the result shall be stored
+ * @param decsep the decimal separator
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtof_lc(str, output, decsep, groupsep) cx_strtof_lc_(cx_strcast(str), output, decsep, groupsep)
+
+/**
+ * Converts a string to a double precision floating point number.
+ *
+ * The function returns non-zero when conversion is not possible.
+ * In that case the function sets errno to EINVAL when the reason is an invalid character.
+ *
+ * @param str the string to convert
+ * @param output a pointer to the double variable where the result shall be stored
+ * @param decsep the decimal separator
+ * @param groupsep each character in this string is treated as group separator and ignored during conversion
+ * @retval zero success
+ * @retval non-zero conversion was not possible
+ */
+#define cx_strtod_lc(str, output, decsep, groupsep) cx_strtod_lc_(cx_strcast(str), output, decsep, groupsep)
 
 /**
  * Converts a string to a single precision floating point number.
@@ -1272,273 +2100,27 @@
  *
  * @param str the string to convert
  * @param output a pointer to the float variable where the result shall be stored
- * @param decsep the decimal separator
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
- * @return zero on success, non-zero if conversion was not possible
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtof_lc(cxstring str, float *output, char decsep, const char *groupsep);
+#define cx_strtof(str, output) cx_strtof_lc_(cx_strcast(str), output, '.', ",")
 
 /**
  * Converts a string to a double precision floating point number.
  *
  * The function returns non-zero when conversion is not possible.
  * In that case the function sets errno to EINVAL when the reason is an invalid character.
- * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
  *
  * The decimal separator is assumed to be a dot character.
  * The comma character is treated as group separator and ignored during parsing.
  * If you want to choose a different format, use cx_strtof_lc().
  *
  * @param str the string to convert
- * @param output a pointer to the float variable where the result shall be stored
- * @param decsep the decimal separator
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
- * @return zero on success, non-zero if conversion was not possible
- */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtod_lc(cxstring str, double *output, char decsep, const char *groupsep);
-
-#ifndef CX_STR_IMPLEMENTATION
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtos_lc(str, output, base, groupsep) cx_strtos_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoi_lc(str, output, base, groupsep) cx_strtoi_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtol_lc(str, output, base, groupsep) cx_strtol_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoll_lc(str, output, base, groupsep) cx_strtoll_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoi8_lc(str, output, base, groupsep) cx_strtoi8_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoi16_lc(str, output, base, groupsep) cx_strtoi16_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoi32_lc(str, output, base, groupsep) cx_strtoi32_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoi64_lc(str, output, base, groupsep) cx_strtoi64_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoz_lc(str, output, base, groupsep) cx_strtoz_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtous_lc(str, output, base, groupsep) cx_strtous_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtou_lc(str, output, base, groupsep) cx_strtou_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoul_lc(str, output, base, groupsep) cx_strtoul_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtoull_lc(str, output, base, groupsep) cx_strtoull_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtou8_lc(str, output, base, groupsep) cx_strtou8_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtou16_lc(str, output, base, groupsep) cx_strtou16_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtou32_lc(str, output, base, groupsep) cx_strtou32_lc(cx_strcast(str), output, base, groupsep)
-/**
- * \copydoc cx_strtouz_lc()
- */
-#define cx_strtou64_lc(str, output, base, groupsep) cx_strtou64_lc(cx_strcast(str), output, base, groupsep)
-/**
- * Converts a string to a number.
- *
- * The function returns non-zero when conversion is not possible.
- * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
- * It sets errno to ERANGE when the target datatype is too small.
- *
- * @param str the string to convert
- * @param output a pointer to the integer variable where the result shall be stored
- * @param base 2, 8, 10, or 16
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
- * @return zero on success, non-zero if conversion was not possible
- */
-#define cx_strtouz_lc(str, output, base, groupsep) cx_strtouz_lc(cx_strcast(str), output, base, groupsep)
-
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtos(str, output, base) cx_strtos_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtoi(str, output, base) cx_strtoi_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtol(str, output, base) cx_strtol_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtoll(str, output, base) cx_strtoll_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtoi8(str, output, base) cx_strtoi8_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtoi16(str, output, base) cx_strtoi16_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtoi32(str, output, base) cx_strtoi32_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
+ * @param output a pointer to the double variable where the result shall be stored
+ * @retval zero success
+ * @retval non-zero conversion was not possible
  */
-#define cx_strtoi64(str, output, base) cx_strtoi64_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtoz(str, output, base) cx_strtoz_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtous(str, output, base) cx_strtous_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtou(str, output, base) cx_strtou_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtoul(str, output, base) cx_strtoul_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtoull(str, output, base) cx_strtoull_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtou8(str, output, base) cx_strtou8_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtou16(str, output, base) cx_strtou16_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtou32(str, output, base) cx_strtou32_lc(str, output, base, ",")
-/**
- * \copydoc cx_strtouz()
- */
-#define cx_strtou64(str, output, base) cx_strtou64_lc(str, output, base, ",")
-/**
- * Converts a string to a number.
- *
- * The function returns non-zero when conversion is not possible.
- * In that case the function sets errno to EINVAL when the reason is an invalid character or an unsupported base.
- * It sets errno to ERANGE when the target datatype is too small.
- *
- * The comma character is treated as group separator and ignored during parsing.
- * If you want to choose the set of group separators, use the \c _lc variant of this function (e.g. cx_strtouz_lc()).
- *
- * @param str the string to convert
- * @param output a pointer to the integer variable where the result shall be stored
- * @param base 2, 8, 10, or 16
- * @return zero on success, non-zero if conversion was not possible
- */
-#define cx_strtouz(str, output, base) cx_strtouz_lc(str, output, base, ",")
-
-/**
- * Converts a string to a single precision floating point number.
- *
- * The function returns non-zero when conversion is not possible.
- * In that case the function sets errno to EINVAL when the reason is an invalid character.
- * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
- *
- * The decimal separator is assumed to be a dot character.
- * The comma character is treated as group separator and ignored during parsing.
- * If you want to choose a different format, use cx_strtof_lc().
- *
- * @param str the string to convert
- * @param output a pointer to the float variable where the result shall be stored
- * @param decsep the decimal separator
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
- * @return zero on success, non-zero if conversion was not possible
- */
-#define cx_strtof_lc(str, output, decsep, groupsep) cx_strtof_lc(cx_strcast(str), output, decsep, groupsep)
-/**
- * Converts a string to a double precision floating point number.
- *
- * The function returns non-zero when conversion is not possible.
- * In that case the function sets errno to EINVAL when the reason is an invalid character.
- * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
- *
- * The decimal separator is assumed to be a dot character.
- * The comma character is treated as group separator and ignored during parsing.
- * If you want to choose a different format, use cx_strtof_lc().
- *
- * @param str the string to convert
- * @param output a pointer to the float variable where the result shall be stored
- * @param decsep the decimal separator
- * @param groupsep each character in this string is treated as group separator and ignored during conversion
- * @return zero on success, non-zero if conversion was not possible
- */
-#define cx_strtod_lc(str, output, decsep, groupsep) cx_strtod_lc(cx_strcast(str), output, decsep, groupsep)
-
-/**
- * Converts a string to a single precision floating point number.
- *
- * The function returns non-zero when conversion is not possible.
- * In that case the function sets errno to EINVAL when the reason is an invalid character.
- * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
- *
- * The decimal separator is assumed to be a dot character.
- * The comma character is treated as group separator and ignored during parsing.
- * If you want to choose a different format, use cx_strtof_lc().
- *
- * @param str the string to convert
- * @param output a pointer to the float variable where the result shall be stored
- * @return zero on success, non-zero if conversion was not possible
- */
-#define cx_strtof(str, output) cx_strtof_lc(str, output, '.', ",")
-/**
- * Converts a string to a double precision floating point number.
- *
- * The function returns non-zero when conversion is not possible.
- * In that case the function sets errno to EINVAL when the reason is an invalid character.
- * It sets errno to ERANGE when the necessary representation would exceed the limits defined in libc's float.h.
- *
- * The decimal separator is assumed to be a dot character.
- * The comma character is treated as group separator and ignored during parsing.
- * If you want to choose a different format, use cx_strtof_lc().
- *
- * @param str the string to convert
- * @param output a pointer to the float variable where the result shall be stored
- * @return zero on success, non-zero if conversion was not possible
- */
-#define cx_strtod(str, output) cx_strtod_lc(str, output, '.', ",")
-
-#endif
+#define cx_strtod(str, output) cx_strtod_lc_(cx_strcast(str), output, '.', ",")
 
 #ifdef __cplusplus
 } // extern "C"
--- a/ucx/cx/test.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/test.h	Tue Feb 25 21:12:11 2025 +0100
@@ -35,13 +35,13 @@
  *
  * **** IN HEADER FILE: ****
  *
- * <pre>
+ * <code>
  * CX_TEST(function_name);
  * CX_TEST_SUBROUTINE(subroutine_name, paramlist); // optional
- * </pre>
+ * </code>
  *
  * **** IN SOURCE FILE: ****
- * <pre>
+ * <code>
  * CX_TEST_SUBROUTINE(subroutine_name, paramlist) {
  *   // tests with CX_TEST_ASSERT()
  * }
@@ -54,7 +54,7 @@
  *   }
  *   // cleanup of memory here
  * }
- * </pre>
+ * </code>
  * 
  * @attention Do not call own functions within a test, that use
  * CX_TEST_ASSERT() macros and are not defined by using CX_TEST_SUBROUTINE().
@@ -87,7 +87,6 @@
 #define __FUNCTION__ __func__
 #endif
 
-//
 #if !defined(__clang__) && __GNUC__ > 3
 #pragma GCC diagnostic ignored "-Wclobbered"
 #endif
@@ -96,7 +95,6 @@
 typedef struct CxTestSuite CxTestSuite;
 
 /** Pointer to a test function. */
-cx_attr_nonnull
 typedef void(*CxTest)(CxTestSuite *, void *, cx_write_func);
 
 /** Type for the internal list of test cases. */
@@ -175,7 +173,8 @@
  * 
  * @param suite the suite, the test function shall be added to
  * @param test the test function to register
- * @return zero on success or non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  */
 cx_attr_nonnull
 static inline int cx_test_register(CxTestSuite* suite, CxTest test) {
@@ -202,7 +201,7 @@
  * Runs a test suite and writes the test log to the specified stream.
  * @param suite the test suite to run
  * @param out_target the target buffer or file to write the output to
- * @param out_writer the write function writing to \p out_target
+ * @param out_writer the write function writing to @p out_target
  */
 cx_attr_nonnull
 static inline void cx_test_run(CxTestSuite *suite,
@@ -231,14 +230,14 @@
 
 /**
  * Runs a test suite and writes the test log to the specified FILE stream.
- * @param suite the test suite to run
- * @param file the target file to write the output to
+ * @param suite (@c CxTestSuite*) the test suite to run
+ * @param file (@c FILE*) the target file to write the output to
  */
 #define cx_test_run_f(suite, file) cx_test_run(suite, (void*)file, (cx_write_func)fwrite)
 
 /**
  * Runs a test suite and writes the test log to stdout.
- * @param suite the test suite to run
+ * @param suite (@c CxTestSuite*) the test suite to run
  */
 #define cx_test_run_stdout(suite) cx_test_run_f(suite, stdout)
 
@@ -253,6 +252,17 @@
 
 /**
  * Defines the scope of a test.
+ *
+ * @code
+ * CX_TEST(my_test_name) {
+ *     // setup code
+ *     CX_TEST_DO {
+ *         // your tests go here
+ *     }
+ *     // tear down code
+ * }
+ * @endcode
+ *
  * @attention Any CX_TEST_ASSERT() calls must be performed in scope of
  * #CX_TEST_DO.
  */
@@ -270,8 +280,8 @@
  * If the assertion is correct, the test carries on. If the assertion is not
  * correct, the specified message (terminated by a dot and a line break) is
  * written to the test suites output stream.
- * @param condition the condition to check
- * @param message the message that shall be printed out on failure
+ * @param condition (@c bool) the condition to check
+ * @param message (@c char*) the message that shall be printed out on failure
  */
 #define CX_TEST_ASSERTM(condition,message) if (!(condition)) { \
         const char *_assert_msg_ = message; \
@@ -286,7 +296,7 @@
  * If the assertion is correct, the test carries on. If the assertion is not
  * correct, the specified message (terminated by a dot and a line break) is
  * written to the test suites output stream.
- * @param condition the condition to check
+ * @param condition (@c bool) the condition to check
  */
 #define CX_TEST_ASSERT(condition) CX_TEST_ASSERTM(condition, #condition " failed")
 
--- a/ucx/cx/tree.h	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/cx/tree.h	Tue Feb 25 21:12:11 2025 +0100
@@ -26,11 +26,11 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 /**
- * \file tree.h
- * \brief Interface for tree implementations.
- * \author Mike Becker
- * \author Olaf Wintermann
- * \copyright 2-Clause BSD License
+ * @file tree.h
+ * @brief Interface for tree implementations.
+ * @author Mike Becker
+ * @author Olaf Wintermann
+ * @copyright 2-Clause BSD License
  */
 
 #ifndef UCX_TREE_H
@@ -138,7 +138,7 @@
      */
     size_t depth;
     /**
-     * The next element in the queue or \c NULL.
+     * The next element in the queue or @c NULL.
      */
     struct cx_tree_visitor_queue_s *next;
 };
@@ -233,7 +233,7 @@
  * Advises the iterator to skip the subtree below the current node and
  * also continues the current loop.
  *
- * @param iterator the iterator
+ * @param iterator (@c CxTreeIterator) the iterator
  */
 #define cxTreeIteratorContinue(iterator) (iterator).skip = true; continue
 
@@ -241,7 +241,7 @@
  * Advises the visitor to skip the subtree below the current node and
  * also continues the current loop.
  *
- * @param visitor the visitor
+ * @param visitor (@c CxTreeVisitor) the visitor
  */
 #define cxTreeVisitorContinue(visitor) cxTreeIteratorContinue(visitor)
 
@@ -249,7 +249,7 @@
  * Links a node to a (new) parent.
  *
  * If the node has already a parent, it is unlinked, first.
- * If the parent has children already, the node is \em appended to the list
+ * If the parent has children already, the node is @em appended to the list
  * of all currently existing children.
  *
  * @param parent the parent node
@@ -263,6 +263,7 @@
  * @see cx_tree_unlink()
  */
 cx_attr_nonnull
+cx_attr_export
 void cx_tree_link(
         void *parent,
         void *node,
@@ -288,6 +289,7 @@
  * @see cx_tree_link()
  */
 cx_attr_nonnull
+cx_attr_export
 void cx_tree_unlink(
         void *node,
         ptrdiff_t loc_parent,
@@ -305,8 +307,8 @@
 /**
  * Function pointer for a search function.
  *
- * A function of this kind shall check if the specified \p node
- * contains the given \p data or if one of the children might contain
+ * A function of this kind shall check if the specified @p node
+ * contains the given @p data or if one of the children might contain
  * the data.
  *
  * The function should use the returned integer to indicate how close the
@@ -335,8 +337,8 @@
 /**
  * Function pointer for a search function.
  *
- * A function of this kind shall check if the specified \p node
- * contains the same \p data as \p new_node or if one of the children might
+ * A function of this kind shall check if the specified @p node
+ * contains the same @p data as @p new_node or if one of the children might
  * contain the data.
  *
  * The function should use the returned integer to indicate how close the
@@ -354,7 +356,7 @@
  * @param node the node that is currently investigated
  * @param new_node a new node with the information which is searched
  *
- * @return 0 if \p node contains the same data as \p new_node,
+ * @return 0 if @p node contains the same data as @p new_node,
  * positive if one of the children might contain the data,
  * negative if neither the node, nor the children contains the data
  */
@@ -370,8 +372,8 @@
  *
  * Depending on the tree structure it is not necessarily guaranteed that the
  * "closest" match is uniquely defined. This function will search for a node
- * with the best match according to the \p sfunc (meaning: the return value of
- * \p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
+ * with the best match according to the @p sfunc (meaning: the return value of
+ * @p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
  * node matching the criteria is returned.
  *
  * @param root the root node
@@ -387,6 +389,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_w(5)
+cx_attr_export
 int cx_tree_search_data(
         const void *root,
         size_t depth,
@@ -406,8 +409,8 @@
  *
  * Depending on the tree structure it is not necessarily guaranteed that the
  * "closest" match is uniquely defined. This function will search for a node
- * with the best match according to the \p sfunc (meaning: the return value of
- * \p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
+ * with the best match according to the @p sfunc (meaning: the return value of
+ * @p sfunc which is closest to zero). If that is also ambiguous, an arbitrary
  * node matching the criteria is returned.
  *
  * @param root the root node
@@ -423,6 +426,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_w(5)
+cx_attr_export
 int cx_tree_search(
         const void *root,
         size_t depth,
@@ -454,6 +458,7 @@
  * @see cxTreeIteratorDispose()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxTreeIterator cx_tree_iterator(
         void *root,
         bool visit_on_exit,
@@ -480,6 +485,7 @@
  * @see cxTreeVisitorDispose()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxTreeVisitor cx_tree_visitor(
         void *root,
         ptrdiff_t loc_children,
@@ -491,9 +497,9 @@
  * The first argument points to the data the node shall contain and
  * the second argument may be used for additional data (e.g. an allocator).
  * Functions of this type shall either return a new pointer to a newly
- * created node or \c NULL when allocation fails.
+ * created node or @c NULL when allocation fails.
  *
- * \note the function may leave the node pointers in the struct uninitialized.
+ * @note the function may leave the node pointers in the struct uninitialized.
  * The caller is responsible to set them according to the intended use case.
  */
 cx_attr_nonnull_arg(1)
@@ -505,6 +511,7 @@
  * 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_attr_export
 extern unsigned int cx_tree_add_look_around_depth;
 
 /**
@@ -513,11 +520,11 @@
  * Once an element cannot be added to the tree, this function returns, leaving
  * the iterator in a valid state pointing to the element that could not be
  * added.
- * Also, the pointer of the created node will be stored to \p failed.
+ * Also, the pointer of the created node will be stored to @p failed.
  * The integer returned by this function denotes the number of elements obtained
- * from the \p iter that have been successfully processed.
- * When all elements could be processed, a \c NULL pointer will be written to
- * \p failed.
+ * from the @p iter that have been successfully processed.
+ * When all elements could be processed, a @c NULL pointer will be written to
+ * @p failed.
  *
  * The advantage of this function compared to multiple invocations of
  * #cx_tree_add() is that the search for the insert locations is not always
@@ -547,6 +554,7 @@
  */
 cx_attr_nonnull_arg(1, 3, 4, 6, 7)
 cx_attr_access_w(6)
+cx_attr_export
 size_t cx_tree_add_iter(
         struct cx_iterator_base_s *iter,
         size_t num,
@@ -566,11 +574,11 @@
  * Adds multiple elements efficiently to a tree.
  *
  * Once an element cannot be added to the tree, this function returns, storing
- * the pointer of the created node to \p failed.
+ * the pointer of the created node to @p failed.
  * The integer returned by this function denotes the number of elements from
- * the \p src array that have been successfully processed.
- * When all elements could be processed, a \c NULL pointer will be written to
- * \p failed.
+ * the @p src array that have been successfully processed.
+ * When all elements could be processed, a @c NULL pointer will be written to
+ * @p failed.
  *
  * The advantage of this function compared to multiple invocations of
  * #cx_tree_add() is that the search for the insert locations is not always
@@ -583,8 +591,8 @@
  * Refer to the documentation of #cx_tree_add() for more details.
  *
  * @param src a pointer to the source data array
- * @param num the number of elements in the \p src array
- * @param elem_size the size of each element in the \p src array
+ * @param num the number of elements in the @p src array
+ * @param elem_size the size of each element in the @p src array
  * @param sfunc a search function
  * @param cfunc a node creation function
  * @param cdata optional additional data
@@ -601,6 +609,7 @@
  */
 cx_attr_nonnull_arg(1, 4, 5, 7, 8)
 cx_attr_access_w(7)
+cx_attr_export
 size_t cx_tree_add_array(
         const void *src,
         size_t num,
@@ -621,28 +630,28 @@
  * Adds data to a tree.
  *
  * An adequate location where to add the new tree node is searched with the
- * specified \p sfunc.
+ * specified @p sfunc.
  *
- * When a location is found, the \p cfunc will be invoked with \p cdata.
+ * When a location is found, the @p cfunc will be invoked with @p cdata.
  *
- * The node returned by \p cfunc will be linked into the tree.
- * When \p sfunc returned a positive integer, the new node will be linked as a
+ * The node returned by @p cfunc will be linked into the tree.
+ * When @p sfunc returned a positive integer, the new node will be linked as a
  * child. The other children (now siblings of the new node) are then checked
- * with \p sfunc, whether they could be children of the new node and re-linked
+ * with @p sfunc, whether they could be children of the new node and re-linked
  * accordingly.
  *
- * When \p sfunc returned zero and the found node has a parent, the new
+ * When @p sfunc returned zero and the found node has a parent, the new
  * node will be added as sibling - otherwise, the new node will be added
  * as a child.
  *
- * When \p sfunc returned a negative value, the new node will not be added to
+ * When @p sfunc returned a negative value, the new node will not be added to
  * the tree and this function returns a non-zero value.
- * The caller should check if \p cnode contains a node pointer and deal with the
+ * The caller should check if @p cnode contains a node pointer and deal with the
  * node that could not be added.
  *
- * This function also returns a non-zero value when \p cfunc tries to allocate
- * a new node but fails to do so. In that case, the pointer stored to \p cnode
- * will be \c NULL.
+ * This function also returns a non-zero value when @p cfunc tries to allocate
+ * a new node but fails to do so. In that case, the pointer stored to @p cnode
+ * will be @c NULL.
  *
  * Multiple elements can be added more efficiently with
  * #cx_tree_add_array() or #cx_tree_add_iter().
@@ -664,6 +673,7 @@
  */
 cx_attr_nonnull_arg(1, 2, 3, 5, 6)
 cx_attr_access_w(5)
+cx_attr_export
 int cx_tree_add(
         const void *src,
         cx_tree_search_func sfunc,
@@ -727,7 +737,7 @@
     /**
      * A pointer to the root node.
      *
-     * Will be \c NULL when \c size is 0.
+     * Will be @c NULL when @c size is 0.
      */
     void *root;
 
@@ -801,6 +811,10 @@
 /**
  * Macro to roll out the #cx_tree_node_base_s structure with a custom
  * node type.
+ *
+ * Must be used as first member in your custom tree struct.
+ *
+ * @param type the data type for the nodes
  */
 #define CX_TREE_NODE_BASE(type) \
     type *parent; \
@@ -811,6 +825,11 @@
 
 /**
  * Macro for specifying the layout of a base node tree.
+ *
+ * When your tree uses #CX_TREE_NODE_BASE, you can use this
+ * macro in all tree functions that expect the layout parameters
+ * @c loc_parent, @c loc_children, @c loc_last_child, @c loc_prev,
+ * and @c loc_next.
  */
 #define cx_tree_node_base_layout \
     offsetof(struct cx_tree_node_base_s, parent),\
@@ -820,26 +839,15 @@
     offsetof(struct cx_tree_node_base_s, next)
 
 /**
- * Macro for obtaining the node pointer layout for a specific tree.
- */
-#define cx_tree_node_layout(tree) \
-    (tree)->loc_parent,\
-    (tree)->loc_children,\
-    (tree)->loc_last_child,\
-    (tree)->loc_prev,  \
-    (tree)->loc_next
-
-/**
  * The class definition for arbitrary trees.
  */
 struct cx_tree_class_s {
     /**
      * Member function for inserting a single element.
      *
-     * Implementations SHALL NOT simply invoke \p insert_many as this comes
+     * Implementations SHALL NOT simply invoke @p insert_many as this comes
      * with too much overhead.
      */
-    cx_attr_nonnull
     int (*insert_element)(
             struct cx_tree_s *tree,
             const void *data
@@ -851,7 +859,6 @@
      * Implementations SHALL avoid to perform a full search in the tree for
      * every element even though the source data MAY be unsorted.
      */
-    cx_attr_nonnull
     size_t (*insert_many)(
             struct cx_tree_s *tree,
             struct cx_iterator_base_s *iter,
@@ -861,7 +868,6 @@
     /**
      * Member function for finding a node.
      */
-    cx_attr_nonnull
     void *(*find)(
             struct cx_tree_s *tree,
             const void *subtree,
@@ -886,10 +892,10 @@
  * tree contents, but - in contrast to #cxTreeFree() - not the tree
  * structure, leaving an empty tree behind.
  *
- * \note The destructor function, if any, will \em not be invoked. That means
+ * @note The destructor function, if any, will @em not be invoked. That means
  * you will need to free the removed subtree by yourself, eventually.
  *
- * \attention This function will not free the memory of the nodes with the
+ * @attention This function will not free the memory of the nodes with the
  * tree's allocator, because that is usually done by the advanced destructor
  * and would therefore result in a double-free.
  *
@@ -898,6 +904,7 @@
  * @see cxTreeFree()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxTreeDestroySubtree(CxTree *tree, void *node);
 
 
@@ -910,7 +917,7 @@
  * This is a convenience macro for invoking #cxTreeDestroySubtree() on the
  * root node of the tree.
  *
- * \attention Be careful when calling this function when no destructor function
+ * @attention Be careful when calling this function when no destructor function
  * is registered that actually frees the memory of nodes. In that case you will
  * need a reference to the (former) root node of the tree somewhere or
  * otherwise you will be leaking memory.
@@ -928,7 +935,7 @@
  * It is guaranteed that for each node the simple destructor is invoked before
  * the advanced destructor.
  *
- * \attention This function will only invoke the destructor functions
+ * @attention This function will only invoke the destructor functions
  * on the nodes.
  * It will NOT additionally free the nodes with the tree's allocator, because
  * that would cause a double-free in most scenarios where the advanced
@@ -936,25 +943,20 @@
  *
  * @param tree the tree to free
  */
-static inline void cxTreeFree(CxTree *tree) {
-    if (tree == NULL) return;
-    if (tree->root != NULL) {
-        cxTreeClear(tree);
-    }
-    cxFree(tree->allocator, tree);
-}
+cx_attr_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.
+ * The specified @p allocator will be used for creating the tree struct
+ * and SHALL be used by @p create_func to allocate memory for the nodes.
  *
- * \note This function will also register an advanced destructor which
+ * @note This function will also register an advanced destructor which
  * will free the nodes with the allocator's free() method.
  *
  * @param allocator the allocator that shall be used
- * (if \c NULL, a default stdlib allocator will be used)
+ * (if @c NULL, a default stdlib allocator will be used)
  * @param create_func a function that creates new nodes
  * @param search_func a function that compares two nodes
  * @param search_data_func a function that compares a node with data
@@ -972,6 +974,7 @@
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxTreeFree, 1)
+cx_attr_export
 CxTree *cxTreeCreate(
         const CxAllocator *allocator,
         cx_tree_node_create_func create_func,
@@ -987,18 +990,18 @@
 /**
  * Creates a new tree structure based on a default layout.
  *
- * Nodes created by \p create_func MUST contain #cx_tree_node_base_s as first
+ * Nodes created by @p create_func MUST contain #cx_tree_node_base_s as first
  * member (or at least respect the default offsets specified in the tree
  * struct) and they MUST be allocated with the specified allocator.
  *
- * \note This function will also register an advanced destructor which
+ * @note This function will also register an advanced destructor which
  * will free the nodes with the allocator's free() method.
  *
- * @param allocator the allocator that shall be used
- * @param create_func a function that creates new nodes
- * @param search_func a function that compares two nodes
- * @param search_data_func a function that compares a node with data
- * @return the new tree
+ * @param allocator (@c CxAllocator*) the allocator that shall be used
+ * @param create_func (@c cx_tree_node_create_func) a function that creates new nodes
+ * @param search_func (@c cx_tree_search_func) a function that compares two nodes
+ * @param search_data_func (@c cx_tree_search_data_func)  a function that compares a node with data
+ * @return (@c CxTree*) the new tree
  * @see cxTreeCreate()
  */
 #define cxTreeCreateSimple(\
@@ -1009,15 +1012,15 @@
 /**
  * Creates a new tree structure based on an existing tree.
  *
- * The specified \p allocator will be used for creating the tree struct.
+ * The specified @p allocator will be used for creating the tree struct.
  *
- * \attention This function will create an incompletely defined tree structure
+ * @attention This function will create an incompletely defined tree structure
  * where neither the create function, the search function, nor a destructor
  * will be set. If you wish to use any of this functionality for the wrapped
  * tree, you need to specify those functions afterwards.
  *
  * @param allocator the allocator that was used for nodes of the wrapped tree
- * (if \c NULL, a default stdlib allocator is assumed)
+ * (if @c NULL, a default stdlib allocator is assumed)
  * @param root the root node of the tree that shall be wrapped
  * @param loc_parent offset in the node struct for the parent pointer
  * @param loc_children offset in the node struct for the children linked list
@@ -1032,6 +1035,7 @@
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxTreeFree, 1)
+cx_attr_export
 CxTree *cxTreeCreateWrapped(
         const CxAllocator *allocator,
         void *root,
@@ -1045,13 +1049,14 @@
 /**
  * Inserts data into the tree.
  *
- * \remark For this function to work, the tree needs specified search and
+ * @remark For this function to work, the tree needs specified search and
  * create functions, which might not be available for wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
  * @param tree the tree
  * @param data the data to insert
- * @return zero on success, non-zero on failure
+ * @retval zero success
+ * @retval non-zero failure
  */
 cx_attr_nonnull
 static inline int cxTreeInsert(
@@ -1064,7 +1069,7 @@
 /**
  * Inserts elements provided by an iterator efficiently into the tree.
  *
- * \remark For this function to work, the tree needs specified search and
+ * @remark For this function to work, the tree needs specified search and
  * create functions, which might not be available for wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
@@ -1076,7 +1081,7 @@
 cx_attr_nonnull
 static inline size_t cxTreeInsertIter(
         CxTree *tree,
-        struct cx_iterator_base_s *iter,
+        CxIteratorBase *iter,
         size_t n
 ) {
     return tree->cl->insert_many(tree, iter, n);
@@ -1085,7 +1090,7 @@
 /**
  * Inserts an array of data efficiently into the tree.
  *
- * \remark For this function to work, the tree needs specified search and
+ * @remark For this function to work, the tree needs specified search and
  * create functions, which might not be available for wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
@@ -1111,13 +1116,13 @@
 /**
  * Searches the data in the specified tree.
  *
- * \remark For this function to work, the tree needs a specified \c search_data
+ * @remark For this function to work, the tree needs a specified @c search_data
  * function, which might not be available wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
  * @param tree the tree
  * @param data the data to search for
- * @return the first matching node, or \c NULL when the data cannot be found
+ * @return the first matching node, or @c NULL when the data cannot be found
  */
 cx_attr_nonnull
 cx_attr_nodiscard
@@ -1131,13 +1136,13 @@
 /**
  * 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.
+ * 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
+ * @note When @p subtree_root is not part of the @p tree, the behavior is
  * undefined.
  *
- * \remark For this function to work, the tree needs a specified \c search_data
+ * @remark For this function to work, the tree needs a specified @c search_data
  * function, which might not be the case for wrapped trees
  * (see #cxTreeCreateWrapped()).
  *
@@ -1145,7 +1150,7 @@
  * @param data the data to search for
  * @param subtree_root the node where to start
  * @param max_depth the maximum search depth
- * @return the first matching node, or \c NULL when the data cannot be found
+ * @return the first matching node, or @c NULL when the data cannot be found
  */
 cx_attr_nonnull
 cx_attr_nodiscard
@@ -1167,6 +1172,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
 
 /**
@@ -1174,10 +1180,11 @@
  *
  * @param tree the tree
  * @param subtree_root the root node of the subtree
- * @return the tree depth including the \p subtree_root
+ * @return the tree depth including the @p subtree_root
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
 
 /**
@@ -1188,10 +1195,11 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 size_t cxTreeDepth(CxTree *tree);
 
 /**
- * Creates a depth-first iterator for the specified tree starting in \p node.
+ * Creates a depth-first iterator for the specified tree starting in @p node.
  *
  * If the node is not part of the tree, the behavior is undefined.
  *
@@ -1216,7 +1224,7 @@
 }
 
 /**
- * Creates a breadth-first iterator for the specified tree starting in \p node.
+ * Creates a breadth-first iterator for the specified tree starting in @p node.
  *
  * If the node is not part of the tree, the behavior is undefined.
  *
@@ -1267,7 +1275,7 @@
 /**
  * Sets the (new) parent of the specified child.
  *
- * If the \p child is not already member of the tree, this function behaves
+ * If the @p child is not already member of the tree, this function behaves
  * as #cxTreeAddChildNode().
  *
  * @param tree the tree
@@ -1276,6 +1284,7 @@
  * @see cxTreeAddChildNode()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxTreeSetParent(
         CxTree *tree,
         void *parent,
@@ -1285,10 +1294,10 @@
 /**
  * Adds a new node to the tree.
  *
- * If the \p child is already member of the tree, the behavior is undefined.
+ * If the @p child is already member of the tree, the behavior is undefined.
  * Use #cxTreeSetParent() if you want to move a subtree to another location.
  *
- * \attention The node may be externally created, but MUST obey the same rules
+ * @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).
  *
@@ -1298,6 +1307,7 @@
  * @see cxTreeSetParent()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxTreeAddChildNode(
         CxTree *tree,
         void *parent,
@@ -1322,6 +1332,7 @@
  * @see cxTreeInsert()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxTreeAddChild(
         CxTree *tree,
         void *parent,
@@ -1352,16 +1363,17 @@
  *
  * If the node is not part of the tree, the behavior is undefined.
  *
- * \note The destructor function, if any, will \em not be invoked. That means
+ * @note The destructor function, if any, will @em not be invoked. That means
  * you will need to free the removed node by yourself, eventually.
  *
  * @param tree the tree
  * @param node the node to remove (must not be the root node)
  * @param relink_func optional callback to update the content of each re-linked
  * node
- * @return zero on success, non-zero if \p node is the root node of the tree
+ * @return zero on success, non-zero if @p node is the root node of the tree
  */
 cx_attr_nonnull_arg(1, 2)
+cx_attr_export
 int cxTreeRemoveNode(
         CxTree *tree,
         void *node,
@@ -1373,13 +1385,14 @@
  *
  * If the node is not part of the tree, the behavior is undefined.
  *
- * \note The destructor function, if any, will \em not be invoked. That means
+ * @note The destructor function, if any, will @em not be invoked. That means
  * you will need to free the removed subtree by yourself, eventually.
  *
  * @param tree the tree
  * @param node the node to remove
  */
 cx_attr_nonnull
+cx_attr_export
 void cxTreeRemoveSubtree(CxTree *tree, void *node);
 
 /**
@@ -1390,7 +1403,7 @@
  * It is guaranteed that the simple destructor is invoked before
  * the advanced destructor.
  *
- * \attention This function will not free the memory of the node with the
+ * @attention This function will not free the memory of the node with the
  * tree's allocator, because that is usually done by the advanced destructor
  * and would therefore result in a double-free.
  *
@@ -1398,9 +1411,10 @@
  * @param node the node to destroy (must not be the root node)
  * @param relink_func optional callback to update the content of each re-linked
  * node
- * @return zero on success, non-zero if \p node is the root node of the tree
+ * @return zero on success, non-zero if @p node is the root node of the tree
  */
 cx_attr_nonnull_arg(1, 2)
+cx_attr_export
 int cxTreeDestroyNode(
         CxTree *tree,
         void *node,
--- a/ucx/hash_map.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/hash_map.c	Tue Feb 25 21:12:11 2025 +0100
@@ -103,7 +103,8 @@
 
     if (elm != NULL && elm->key.hash == hash && elm->key.len == key.len &&
         memcmp(elm->key.data, key.data, key.len) == 0) {
-        // overwrite existing element
+        // overwrite existing element, but call destructors first
+        cx_invoke_destructor(map, elm->data);
         if (map->collection.store_pointer) {
             memcpy(elm->data, &value, sizeof(void *));
         } else {
@@ -169,13 +170,13 @@
 /**
  * Helper function to avoid code duplication.
  *
- * If \p remove is true, and \p targetbuf is \c NULL, the element
+ * If @p remove is true, and @p targetbuf is @c NULL, the element
  * will be destroyed when found.
  *
- * If \p remove is true, and \p targetbuf is set, the element will
+ * If @p remove is true, and @p targetbuf is set, the element will
  * be copied to that buffer and no destructor function is called.
  *
- * If \p remove is false, \p targetbuf must not be non-null and
+ * If @p remove is false, @p targetbuf must not be non-null and
  * either the pointer, when the map is storing pointers, is copied
  * to the target buffer, or a pointer to the stored object will
  * be copied to the target buffer.
@@ -252,22 +253,22 @@
 }
 
 static void *cx_hash_map_iter_current_entry(const void *it) {
-    const struct cx_iterator_s *iter = it;
-    // struct has to have a compatible signature
-    return (struct cx_map_entry_s *) &(iter->kv_data);
+    const CxMapIterator *iter = it;
+    // we have to cast away const, because of the signature
+    return (void*) &iter->entry;
 }
 
 static void *cx_hash_map_iter_current_key(const void *it) {
-    const struct cx_iterator_s *iter = it;
-    struct cx_hash_map_element_s *elm = iter->elem_handle;
+    const CxMapIterator *iter = it;
+    struct cx_hash_map_element_s *elm = iter->elem;
     return &elm->key;
 }
 
 static void *cx_hash_map_iter_current_value(const void *it) {
-    const struct cx_iterator_s *iter = it;
-    const struct cx_hash_map_s *map = iter->src_handle.c;
-    struct cx_hash_map_element_s *elm = iter->elem_handle;
-    if (map->base.collection.store_pointer) {
+    const CxMapIterator *iter = it;
+    const CxMap *map = iter->map.c;
+    struct cx_hash_map_element_s *elm = iter->elem;
+    if (map->collection.store_pointer) {
         return *(void **) elm->data;
     } else {
         return elm->data;
@@ -275,14 +276,15 @@
 }
 
 static bool cx_hash_map_iter_valid(const void *it) {
-    const struct cx_iterator_s *iter = it;
-    return iter->elem_handle != NULL;
+    const CxMapIterator *iter = it;
+    return iter->elem != NULL;
 }
 
 static void cx_hash_map_iter_next(void *it) {
-    struct cx_iterator_s *iter = it;
-    struct cx_hash_map_element_s *elm = iter->elem_handle;
-    struct cx_hash_map_s *map = iter->src_handle.m;
+    CxMapIterator *iter = it;
+    CxMap *map = iter->map.m;
+    struct cx_hash_map_s *hmap = (struct cx_hash_map_s *) map;
+    struct cx_hash_map_element_s *elm = iter->elem;
 
     // remove current element, if asked
     if (iter->base.remove) {
@@ -295,18 +297,18 @@
 
         // search the previous element
         struct cx_hash_map_element_s *prev = NULL;
-        if (map->buckets[iter->slot] != elm) {
-            prev = map->buckets[iter->slot];
+        if (hmap->buckets[iter->slot] != elm) {
+            prev = hmap->buckets[iter->slot];
             while (prev->next != elm) {
                 prev = prev->next;
             }
         }
 
         // destroy
-        cx_invoke_destructor((struct cx_map_s *) map, elm->data);
+        cx_invoke_destructor(map, elm->data);
 
         // unlink
-        cx_hash_map_unlink(map, iter->slot, prev, elm);
+        cx_hash_map_unlink(hmap, iter->slot, prev, elm);
 
         // advance
         elm = next;
@@ -317,32 +319,31 @@
     }
 
     // search the next bucket, if required
-    while (elm == NULL && ++iter->slot < map->bucket_count) {
-        elm = map->buckets[iter->slot];
+    while (elm == NULL && ++iter->slot < hmap->bucket_count) {
+        elm = hmap->buckets[iter->slot];
     }
+    iter->elem = elm;
 
-    // fill the struct with the next element
-    iter->elem_handle = elm;
-    if (elm == NULL) {
-        iter->kv_data.key = NULL;
-        iter->kv_data.value = NULL;
-    } else {
-        iter->kv_data.key = &elm->key;
-        if (map->base.collection.store_pointer) {
-            iter->kv_data.value = *(void **) elm->data;
+    // copy data to a location where the iterator can point to
+    // we need to do it here, because the iterator function call
+    // must not modify the iterator (the parameter is const)
+    if (elm != NULL) {
+        iter->entry.key = &elm->key;
+        if (iter->map.c->collection.store_pointer) {
+            iter->entry.value = *(void **) elm->data;
         } else {
-            iter->kv_data.value = elm->data;
+            iter->entry.value = elm->data;
         }
     }
 }
 
-static CxIterator cx_hash_map_iterator(
+static CxMapIterator cx_hash_map_iterator(
         const CxMap *map,
         enum cx_map_iterator_type type
 ) {
-    CxIterator iter;
+    CxMapIterator iter;
 
-    iter.src_handle.c = map;
+    iter.map.c = map;
     iter.elem_count = map->collection.size;
 
     switch (type) {
@@ -376,17 +377,15 @@
         while (elm == NULL) {
             elm = hash_map->buckets[++iter.slot];
         }
-        iter.elem_handle = elm;
-        iter.kv_data.key = &elm->key;
+        iter.elem = elm;
+        iter.entry.key = &elm->key;
         if (map->collection.store_pointer) {
-            iter.kv_data.value = *(void **) elm->data;
+            iter.entry.value = *(void **) elm->data;
         } else {
-            iter.kv_data.value = elm->data;
+            iter.entry.value = elm->data;
         }
     } else {
-        iter.elem_handle = NULL;
-        iter.kv_data.key = NULL;
-        iter.kv_data.value = NULL;
+        iter.elem = NULL;
     }
 
     return iter;
@@ -433,11 +432,10 @@
     map->base.collection.allocator = allocator;
 
     if (itemsize > 0) {
-        map->base.collection.store_pointer = false;
         map->base.collection.elem_size = itemsize;
     } else {
+        map->base.collection.elem_size = sizeof(void *);
         map->base.collection.store_pointer = true;
-        map->base.collection.elem_size = sizeof(void *);
     }
 
     return (CxMap *) map;
--- a/ucx/json.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/json.c	Tue Feb 25 21:12:11 2025 +0100
@@ -27,13 +27,10 @@
  */
 
 #include "cx/json.h"
-#include "cx/compare.h"
 
 #include <string.h>
-#include <ctype.h>
 #include <assert.h>
 #include <stdio.h>
-#include <errno.h>
 #include <inttypes.h>
 
 /*
@@ -135,6 +132,16 @@
     }
 }
 
+static bool json_isdigit(char c) {
+    // TODO: remove once UCX has public API for this
+    return c >= '0' && c <= '9';
+}
+
+static bool json_isspace(char c) {
+    // TODO: remove once UCX has public API for this
+    return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\v' || c == '\f';
+}
+
 static int num_isexp(const char *content, size_t length, size_t pos) {
     if (pos >= length) {
         return 0;
@@ -143,7 +150,7 @@
     int ok = 0;
     for (size_t i = pos; i < length; i++) {
         char c = content[i];
-        if (isdigit(c)) {
+        if (json_isdigit(c)) {
             ok = 1;
         } else if (i == pos) {
             if (!(c == '+' || c == '-')) {
@@ -160,7 +167,7 @@
 static CxJsonTokenType token_numbertype(const char *content, size_t length) {
     if (length == 0) return CX_JSON_TOKEN_ERROR;
 
-    if (content[0] != '-' && !isdigit(content[0])) {
+    if (content[0] != '-' && !json_isdigit(content[0])) {
         return CX_JSON_TOKEN_ERROR;
     }
 
@@ -173,7 +180,7 @@
             type = CX_JSON_TOKEN_NUMBER;
         } else if (content[i] == 'e' || content[i] == 'E') {
             return num_isexp(content, length, i + 1) ? CX_JSON_TOKEN_NUMBER : CX_JSON_TOKEN_ERROR;
-        } else if (!isdigit(content[i])) {
+        } else if (!json_isdigit(content[i])) {
             return CX_JSON_TOKEN_ERROR; // char is not a digit, decimal separator or exponent sep
         }
     }
@@ -237,7 +244,7 @@
             return CX_JSON_TOKEN_STRING;
         }
         default: {
-            if (isspace(c)) {
+            if (json_isspace(c)) {
                 return CX_JSON_TOKEN_SPACE;
             }
         }
@@ -254,7 +261,10 @@
 
     // current token type and start index
     CxJsonTokenType ttype = json->uncompleted.tokentype;
-    size_t token_start = json->buffer.pos;
+    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] == '\\';
 
     for (size_t i = json->buffer.pos; i < json->buffer.size; i++) {
         char c = json->buffer.space[i];
@@ -268,7 +278,7 @@
                 } else if (ctype == CX_JSON_TOKEN_STRING) {
                     // begin string
                     ttype = CX_JSON_TOKEN_STRING;
-                    token_start = i;
+                    token_part_start = i;
                 } else if (ctype != CX_JSON_NO_TOKEN) {
                     // single-char token
                     json->buffer.pos = i + 1;
@@ -276,12 +286,12 @@
                     return CX_JSON_NO_ERROR;
                 } else {
                     ttype = CX_JSON_TOKEN_LITERAL; // number or literal
-                    token_start = i;
+                    token_part_start = i;
                 }
             } else {
                 // finish token
                 if (ctype != CX_JSON_NO_TOKEN) {
-                    *result = token_create(json, false, token_start, i);
+                    *result = token_create(json, false, token_part_start, i);
                     if (result->tokentype == CX_JSON_NO_TOKEN) {
                         return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
                     }
@@ -294,18 +304,18 @@
             }
         } else {
             // currently inside a string
-            if (json->tokenizer_escape) {
-                json->tokenizer_escape = false;
+            if (escape_end_of_string) {
+                escape_end_of_string = false;
             } else {
                 if (c == '"') {
-                    *result = token_create(json, true, token_start, i + 1);
+                    *result = token_create(json, true, token_part_start, i + 1);
                     if (result->tokentype == CX_JSON_NO_TOKEN) {
                         return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
                     }
                     json->buffer.pos = i + 1;
                     return CX_JSON_NO_ERROR;
                 } else if (c == '\\') {
-                    json->tokenizer_escape = true;
+                    escape_end_of_string = true;
                 }
             }
         }
@@ -313,13 +323,13 @@
 
     if (ttype != CX_JSON_NO_TOKEN) {
         // uncompleted token
-        size_t uncompleted_len = json->buffer.size - token_start;
+        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_start, uncompleted_len))
+                cx_strdup(cx_strn(json->buffer.space + token_part_start, uncompleted_len))
             };
             if (uncompleted.content.ptr == NULL) {
                 return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
@@ -330,7 +340,7 @@
             // combine the uncompleted token with the current token
             assert(json->uncompleted.allocated);
             cxmutstr str = cx_strcat_m(json->uncompleted.content, 1,
-                cx_strn(json->buffer.space + token_start, uncompleted_len));
+                cx_strn(json->buffer.space + token_part_start, uncompleted_len));
             if (str.ptr == NULL) {
                 return CX_JSON_BUFFER_ALLOC_FAILED; // LCOV_EXCL_LINE
             }
@@ -343,9 +353,75 @@
     return CX_JSON_INCOMPLETE_DATA;
 }
 
+// converts a Unicode codepoint to utf8
+static unsigned codepoint_to_utf8(uint32_t codepoint, char *output_buf) {
+    if (codepoint <= 0x7F) {
+        *output_buf = (char)codepoint;
+        return 1;
+    } else if (codepoint <= 0x7FF) {
+        output_buf[0] = (char)(0xC0 | ((codepoint >> 6) & 0x1F));
+        output_buf[1] = (char)(0x80 | (codepoint & 0x3F));
+        return 2;
+    } else if (codepoint <= 0xFFFF) {
+        output_buf[0] = (char)(0xE0 | ((codepoint >> 12) & 0x0F));
+        output_buf[1] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
+        output_buf[2] = (char)(0x80 | (codepoint & 0x3F));
+        return 3;
+    } else if (codepoint <= 0x10FFFF) {
+        output_buf[0] = (char)(0xF0 | ((codepoint >> 18) & 0x07));
+        output_buf[1] = (char)(0x80 | ((codepoint >> 12) & 0x3F));
+        output_buf[2] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
+        output_buf[3] = (char)(0x80 | (codepoint & 0x3F));
+        return 4;
+    }
+    
+    return 0; // LCOV_EXCL_LINE
+}
+
+// converts a utf16 surrogate pair to utf8
+static inline uint32_t utf16pair_to_codepoint(uint16_t c0, uint16_t c1) {
+    return ((c0 - 0xD800) << 10) + (c1 - 0xDC00) + 0x10000;
+}
+
+static unsigned unescape_unicode_string(cxstring str, char *utf8buf) {
+    // str is supposed to start with "\uXXXX" or "\uXXXX\uXXXX"
+    // remaining bytes in the string are ignored (str may be larger!)
+
+    if (str.length < 6 || str.ptr[0] != '\\' || str.ptr[1] != 'u') {
+        return 0;
+    }
+
+    unsigned utf8len = 0;
+    cxstring ustr1 = { str.ptr + 2, 4};
+    uint16_t utf16a, utf16b;
+    if (!cx_strtou16_lc(ustr1, &utf16a, 16, "")) {
+        uint32_t codepoint;
+        if (utf16a < 0xD800 || utf16a > 0xE000) {
+            // character is in the Basic Multilingual Plane
+            // and encoded as a single utf16 char
+            codepoint = utf16a;
+            utf8len = codepoint_to_utf8(codepoint, utf8buf);
+        } else if (utf16a >= 0xD800 && utf16a <= 0xDBFF) {
+            // character is encoded as a surrogate pair
+            // get next 6 bytes
+            if (str.length >= 12) {
+                if (str.ptr[6] == '\\' && str.ptr[7] == 'u') {
+                    cxstring ustr2 = { str.ptr+8, 4 };
+                    if (!cx_strtou16_lc(ustr2, &utf16b, 16, "")
+                            && utf16b >= 0xDC00 && utf16b <= 0xDFFF) {
+                        codepoint = utf16pair_to_codepoint(utf16a, utf16b);
+                        utf8len = codepoint_to_utf8(codepoint, utf8buf);
+                    }
+                }
+            }
+        }
+    }
+    return utf8len;
+}
+
 static cxmutstr unescape_string(const CxAllocator *a, cxmutstr str) {
-    // TODO: support more escape sequences
-    // we know that the unescaped string will be shorter by at least 2 chars
+    // note: this function expects that str contains the enclosing quotes!
+
     cxmutstr result;
     result.length = 0;
     result.ptr = cxMalloc(a, str.length - 1);
@@ -358,9 +434,45 @@
             u = false;
             if (c == 'n') {
                 c = '\n';
+            } else if (c == '"') {
+                c = '"';
             } else if (c == 't') {
                 c = '\t';
+            } else if (c == 'r') {
+                c = '\r';
+            } else if (c == '\\') {
+                c = '\\';
+            } else if (c == '/') {
+                c = '/'; // always unescape, we don't need settings here
+            } else if (c == 'f') {
+                c = '\f';
+            } else if (c == 'b') {
+                c = '\b';
+            } else if (c == 'u') {
+                char utf8buf[4];
+                unsigned utf8len = unescape_unicode_string(
+                    cx_strn(str.ptr + i - 1, str.length + 1 - i),
+                    utf8buf
+                );
+                if(utf8len > 0) {
+                    i += utf8len < 4 ? 4 : 10;
+                    // add all bytes from utf8buf except the last char
+                    // to the result (last char will be added below)
+                    utf8len--;
+                    c = utf8buf[utf8len];
+                    for (unsigned x = 0; x < utf8len; x++) {
+                        result.ptr[result.length++] = utf8buf[x];
+                    }
+                } else {
+                    // decoding failed, ignore the entire sequence
+                    result.ptr[result.length++] = '\\';
+                }
+            } else {
+                // TODO: discuss the behavior for unrecognized escape sequences
+                //       most parsers throw an error here - we just ignore it
+                result.ptr[result.length++] = '\\';
             }
+
             result.ptr[result.length++] = c;
         } else {
             if (c == '\\') {
@@ -375,7 +487,60 @@
     return result;
 }
 
-static CxJsonValue* create_json_value(CxJson *json, CxJsonValueType type) {
+static cxmutstr escape_string(cxmutstr str, bool escape_slash) {
+    // note: this function produces the string without enclosing quotes
+    // the reason is that we don't want to allocate memory just for that
+    CxBuffer buf = {0};
+
+    bool all_printable = true;
+    for (size_t i = 0; i < str.length; i++) {
+        unsigned char c = str.ptr[i];
+        bool escape = c < 0x20 || c == '\\' || c == '"'
+            || (escape_slash && c == '/');
+
+        if (all_printable && escape) {
+            size_t capa = str.length + 32;
+            char *space = malloc(capa);
+            if (space == NULL) return cx_mutstrn(NULL, 0);
+            cxBufferInit(&buf, space, capa, NULL, CX_BUFFER_AUTO_EXTEND);
+            cxBufferWrite(str.ptr, 1, i, &buf);
+            all_printable = false;
+        }
+        if (escape) {
+            cxBufferPut(&buf, '\\');
+            if (c == '\"') {
+                cxBufferPut(&buf, '\"');
+            } else if (c == '\n') {
+                cxBufferPut(&buf, 'n');
+            } else if (c == '\t') {
+                cxBufferPut(&buf, 't');
+            } else if (c == '\r') {
+                cxBufferPut(&buf, 'r');
+            } else if (c == '\\') {
+                cxBufferPut(&buf, '\\');
+            } else if (c == '/') {
+                cxBufferPut(&buf, '/');
+            } else if (c == '\f') {
+                cxBufferPut(&buf, 'f');
+            } else if (c == '\b') {
+                cxBufferPut(&buf, 'b');
+            } else {
+                char code[6];
+                snprintf(code, sizeof(code), "u%04x", (unsigned int) c);
+                cxBufferPutString(&buf, code);
+            }
+        } else if (!all_printable) {
+            cxBufferPut(&buf, c);
+        }
+    }
+    if (!all_printable) {
+        str = cx_mutstrn(buf.space, buf.size);
+    }
+    cxBufferDestroy(&buf);
+    return str;
+}
+
+static CxJsonValue* json_create_value(CxJson *json, CxJsonValueType type) {
     CxJsonValue *v = cxCalloc(json->allocator, 1, sizeof(CxJsonValue));
     if (v == NULL) return NULL; // LCOV_EXCL_LINE
 
@@ -541,21 +706,21 @@
         json_add_state(json, 10 + state);
         switch (token.tokentype) {
             case CX_JSON_TOKEN_BEGIN_ARRAY: {
-                if (create_json_value(json, CX_JSON_ARRAY) == NULL) {
+                if (json_create_value(json, CX_JSON_ARRAY) == NULL) {
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 json_add_state(json, JP_STATE_VALUE_BEGIN_AR);
                 return_rec(CX_JSON_NO_ERROR);
             }
             case CX_JSON_TOKEN_BEGIN_OBJECT: {
-                if (create_json_value(json, CX_JSON_OBJECT) == NULL) {
+                if (json_create_value(json, CX_JSON_OBJECT) == NULL) {
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 json_add_state(json, JP_STATE_OBJ_NAME_OR_CLOSE);
                 return_rec(CX_JSON_NO_ERROR);
             }
             case CX_JSON_TOKEN_STRING: {
-                if ((vbuf = create_json_value(json, CX_JSON_STRING)) == NULL) {
+                if ((vbuf = json_create_value(json, CX_JSON_STRING)) == NULL) {
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 cxmutstr str = unescape_string(json->allocator, token.content);
@@ -568,7 +733,7 @@
             case CX_JSON_TOKEN_INTEGER:
             case CX_JSON_TOKEN_NUMBER: {
                 int type = token.tokentype == CX_JSON_TOKEN_INTEGER ? CX_JSON_INTEGER : CX_JSON_NUMBER;
-                if (NULL == (vbuf = create_json_value(json, type))) {
+                if (NULL == (vbuf = json_create_value(json, type))) {
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 if (type == CX_JSON_INTEGER) {
@@ -583,7 +748,7 @@
                 return_rec(CX_JSON_NO_ERROR);
             }
             case CX_JSON_TOKEN_LITERAL: {
-                if ((vbuf = create_json_value(json, CX_JSON_LITERAL)) == NULL) {
+                if ((vbuf = json_create_value(json, CX_JSON_LITERAL)) == NULL) {
                     return_rec(CX_JSON_VALUE_ALLOC_FAILED); // LCOV_EXCL_LINE
                 }
                 if (0 == cx_strcmp(cx_strcast(token.content), cx_str("true"))) {
@@ -734,6 +899,7 @@
 }
 
 CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator) {
+    if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
     v->allocator = allocator;
@@ -755,6 +921,7 @@
 }
 
 CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator) {
+    if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
     v->allocator = allocator;
@@ -765,6 +932,7 @@
 }
 
 CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num) {
+    if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
     v->allocator = allocator;
@@ -774,6 +942,7 @@
 }
 
 CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num) {
+    if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
     v->allocator = allocator;
@@ -787,6 +956,7 @@
 }
 
 CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str) {
+    if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
     v->allocator = allocator;
@@ -798,6 +968,7 @@
 }
 
 CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit) {
+    if (allocator == NULL) allocator = cxDefaultAllocator;
     CxJsonValue* v = cxMalloc(allocator, sizeof(CxJsonValue));
     if (v == NULL) return NULL;
     v->allocator = allocator;
@@ -808,7 +979,7 @@
 
 // LCOV_EXCL_START
 // never called as long as malloc() does not return NULL
-static void cx_json_arr_free_temp(CxJsonValue** values, size_t count) {
+static void json_arr_free_temp(CxJsonValue** values, size_t count) {
     for (size_t i = 0; i < count; i++) {
         if (values[i] == NULL) break;
         cxJsonValueFree(values[i]);
@@ -822,7 +993,7 @@
     if (values == NULL) return -1;
     for (size_t i = 0; i < count; i++) {
         values[i] = cxJsonCreateNumber(arr->allocator, num[i]);
-        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+        if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; }
     }
     int ret = cxJsonArrAddValues(arr, values, count);
     free(values);
@@ -834,7 +1005,7 @@
     if (values == NULL) return -1;
     for (size_t i = 0; i < count; i++) {
         values[i] = cxJsonCreateInteger(arr->allocator, num[i]);
-        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+        if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; }
     }
     int ret = cxJsonArrAddValues(arr, values, count);
     free(values);
@@ -846,7 +1017,7 @@
     if (values == NULL) return -1;
     for (size_t i = 0; i < count; i++) {
         values[i] = cxJsonCreateString(arr->allocator, str[i]);
-        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+        if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; }
     }
     int ret = cxJsonArrAddValues(arr, values, count);
     free(values);
@@ -858,7 +1029,7 @@
     if (values == NULL) return -1;
     for (size_t i = 0; i < count; i++) {
         values[i] = cxJsonCreateCxString(arr->allocator, str[i]);
-        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+        if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; }
     }
     int ret = cxJsonArrAddValues(arr, values, count);
     free(values);
@@ -870,7 +1041,7 @@
     if (values == NULL) return -1;
     for (size_t i = 0; i < count; i++) {
         values[i] = cxJsonCreateLiteral(arr->allocator, lit[i]);
-        if (values[i] == NULL) { cx_json_arr_free_temp(values, count); return -1; }
+        if (values[i] == NULL) { json_arr_free_temp(values, count); return -1; }
     }
     int ret = cxJsonArrAddValues(arr, values, count);
     free(values);
@@ -979,25 +1150,25 @@
     }
 }
 
-static const CxJsonWriter cx_json_writer_default = {
-    false,
-    true,
-    255,
-    false,
-    4
-};
-
 CxJsonWriter cxJsonWriterCompact(void) {
-    return cx_json_writer_default;
+    return (CxJsonWriter) {
+        false,
+        true,
+        6,
+        false,
+        4,
+        false
+    };
 }
 
 CxJsonWriter cxJsonWriterPretty(bool use_spaces) {
     return (CxJsonWriter) {
         true,
         true,
-        255,
+        6,
         use_spaces,
-        4
+        4,
+        false
     };
 }
 
@@ -1044,7 +1215,7 @@
     size_t actual = 0, expected = 0;
 
     // small buffer for number to string conversions
-    char numbuf[32];
+    char numbuf[40];
 
     // recursively write the values
     switch (value->type) {
@@ -1078,9 +1249,11 @@
 
                 // the name
                 actual += wfunc("\"", 1, 1, target);
-                // TODO: escape the string
-                actual += wfunc(member->name.ptr, 1,
-                    member->name.length, target);
+                cxmutstr name = escape_string(member->name, settings->escape_slash);
+                actual += wfunc(name.ptr, 1, name.length, target);
+                if (name.ptr != member->name.ptr) {
+                    cx_strfree(&name);
+                }
                 actual += wfunc("\"", 1, 1, target);
                 const char *obj_name_sep = ": ";
                 if (settings->pretty) {
@@ -1146,20 +1319,81 @@
         }
         case CX_JSON_STRING: {
             actual += wfunc("\"", 1, 1, target);
-            // TODO: escape the string
-            actual += wfunc(value->value.string.ptr, 1,
-                value->value.string.length, target);
+            cxmutstr str = escape_string(value->value.string, settings->escape_slash);
+            actual += wfunc(str.ptr, 1, str.length, target);
+            if (str.ptr != value->value.string.ptr) {
+                cx_strfree(&str);
+            }
             actual += wfunc("\"", 1, 1, target);
             expected += 2 + value->value.string.length;
             break;
         }
         case CX_JSON_NUMBER: {
-            // TODO: locale bullshit
-            // TODO: formatting settings
-            snprintf(numbuf, 32, "%g", value->value.number);
-            size_t len = strlen(numbuf);
-            actual += wfunc(numbuf, 1, len, target);
-            expected += len;
+            int precision = settings->frac_max_digits;
+            // because of the way how %g is defined, we need to
+            // double the precision and truncate ourselves
+            precision = 1 + (precision > 15 ? 30 : 2 * precision);
+            snprintf(numbuf, 40, "%.*g", precision, value->value.number);
+            char *dot, *exp;
+            unsigned char max_digits;
+            // find the decimal separator and hope that it's one of . or ,
+            dot = strchr(numbuf, '.');
+            if (dot == NULL) {
+                dot = strchr(numbuf, ',');
+            }
+            if (dot == NULL) {
+                // no decimal separator found
+                // output everything until a possible exponent
+                max_digits = 30;
+                dot = numbuf;
+            } else {
+                // found a decimal separator
+                // output everything until the separator
+                // and set max digits to what the settings say
+                size_t len = dot - numbuf;
+                actual += wfunc(numbuf, 1, len, target);
+                expected += len;
+                max_digits = settings->frac_max_digits;
+                if (max_digits > 15) {
+                    max_digits = 15;
+                }
+                // locale independent separator
+                if (max_digits > 0) {
+                    actual += wfunc(".", 1, 1, target);
+                    expected++;
+                }
+                dot++;
+            }
+            // find the exponent
+            exp = strchr(dot, 'e');
+            if (exp == NULL) {
+                // no exponent - output the rest
+                if (max_digits > 0) {
+                    size_t len = strlen(dot);
+                    if (len > max_digits) {
+                        len = max_digits;
+                    }
+                    actual += wfunc(dot, 1, len, target);
+                    expected += len;
+                }
+            } else {
+                // exponent found - truncate the frac digits
+                // and then output the rest
+                if (max_digits > 0) {
+                    size_t len = exp - dot - 1;
+                    if (len > max_digits) {
+                        len = max_digits;
+                    }
+                    actual += wfunc(dot, 1, len, target);
+                    expected += len;
+                }
+                actual += wfunc("e", 1, 1, target);
+                expected++;
+                exp++;
+                size_t len = strlen(exp);
+                actual += wfunc(exp, 1, len, target);
+                expected += len;
+            }
             break;
         }
         case CX_JSON_INTEGER: {
@@ -1201,12 +1435,13 @@
     cx_write_func wfunc,
     const CxJsonWriter *settings
 ) {
-    if (settings == NULL) {
-        settings = &cx_json_writer_default;
-    }
     assert(target != NULL);
     assert(value != NULL);
     assert(wfunc != NULL);
 
+    CxJsonWriter writer_default = cxJsonWriterCompact();
+    if (settings == NULL) {
+        settings = &writer_default;
+    }
     return cx_json_write_rec(target, value, wfunc, settings, 0);
 }
--- a/ucx/linked_list.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/linked_list.c	Tue Feb 25 21:12:11 2025 +0100
@@ -56,48 +56,33 @@
     return (void *) cur;
 }
 
-ssize_t cx_linked_list_find(
+void *cx_linked_list_find(
         const void *start,
         ptrdiff_t loc_advance,
         ptrdiff_t loc_data,
         cx_compare_func cmp_func,
-        const void *elem
+        const void *elem,
+        size_t *found_index
 ) {
-    void *dummy;
-    return cx_linked_list_find_node(
-            &dummy, start,
-            loc_advance, loc_data,
-            cmp_func, elem
-    );
-}
-
-ssize_t cx_linked_list_find_node(
-        void **result,
-        const void *start,
-        ptrdiff_t loc_advance,
-        ptrdiff_t loc_data,
-        cx_compare_func cmp_func,
-        const void *elem
-) {
-    assert(result != NULL);
     assert(start != NULL);
     assert(loc_advance >= 0);
     assert(loc_data >= 0);
     assert(cmp_func);
 
-    const void *node = start;
-    ssize_t index = 0;
+    void *node = (void*) start;
+    size_t index = 0;
     do {
         void *current = ll_data(node);
         if (cmp_func(current, elem) == 0) {
-            *result = (void *) node;
-            return index;
+            if (found_index != NULL) {
+                *found_index = index;
+            }
+            return node;
         }
         node = ll_advance(node);
         index++;
     } while (node != NULL);
-    *result = NULL;
-    return -1;
+    return NULL;
 }
 
 void *cx_linked_list_first(
@@ -811,11 +796,6 @@
     list->collection.size = 0;
 }
 
-#ifndef CX_LINKED_LIST_SWAP_SBO_SIZE
-#define CX_LINKED_LIST_SWAP_SBO_SIZE 128
-#endif
-const unsigned cx_linked_list_swap_sbo_size = CX_LINKED_LIST_SWAP_SBO_SIZE;
-
 static int cx_ll_swap(
         struct cx_list_s *list,
         size_t i,
@@ -894,41 +874,33 @@
         }
     }
 
-    if (list->collection.elem_size > CX_LINKED_LIST_SWAP_SBO_SIZE) {
-        cx_linked_list_node *prev = nleft->prev;
-        cx_linked_list_node *next = nright->next;
-        cx_linked_list_node *midstart = nleft->next;
-        cx_linked_list_node *midend = nright->prev;
+    cx_linked_list_node *prev = nleft->prev;
+    cx_linked_list_node *next = nright->next;
+    cx_linked_list_node *midstart = nleft->next;
+    cx_linked_list_node *midend = nright->prev;
 
-        if (prev == NULL) {
-            ll->begin = nright;
-        } else {
-            prev->next = nright;
-        }
-        nright->prev = prev;
-        if (midstart == nright) {
-            // special case: both nodes are adjacent
-            nright->next = nleft;
-            nleft->prev = nright;
-        } else {
-            // likely case: a chain is between the two nodes
-            nright->next = midstart;
-            midstart->prev = nright;
-            midend->next = nleft;
-            nleft->prev = midend;
-        }
-        nleft->next = next;
-        if (next == NULL) {
-            ll->end = nleft;
-        } else {
-            next->prev = nleft;
-        }
+    if (prev == NULL) {
+        ll->begin = nright;
+    } else {
+        prev->next = nright;
+    }
+    nright->prev = prev;
+    if (midstart == nright) {
+        // special case: both nodes are adjacent
+        nright->next = nleft;
+        nleft->prev = nright;
     } else {
-        // swap payloads to avoid relinking
-        char buf[CX_LINKED_LIST_SWAP_SBO_SIZE];
-        memcpy(buf, nleft->payload, list->collection.elem_size);
-        memcpy(nleft->payload, nright->payload, list->collection.elem_size);
-        memcpy(nright->payload, buf, list->collection.elem_size);
+        // likely case: a chain is between the two nodes
+        nright->next = midstart;
+        midstart->prev = nright;
+        midend->next = nleft;
+        nleft->prev = midend;
+    }
+    nleft->next = next;
+    if (next == NULL) {
+        ll->end = nleft;
+    } else {
+        next->prev = nleft;
     }
 
     return 0;
@@ -943,35 +915,30 @@
     return node == NULL ? NULL : node->payload;
 }
 
-static ssize_t cx_ll_find_remove(
+static size_t cx_ll_find_remove(
         struct cx_list_s *list,
         const void *elem,
         bool remove
 ) {
+    size_t index;
+    cx_linked_list *ll = ((cx_linked_list *) list);
+    cx_linked_list_node *node = cx_linked_list_find(
+            ll->begin,
+            CX_LL_LOC_NEXT, CX_LL_LOC_DATA,
+            list->collection.cmpfunc, elem,
+            &index
+    );
+    if (node == NULL) {
+        return list->collection.size;
+    }
     if (remove) {
-        cx_linked_list *ll = ((cx_linked_list *) list);
-        cx_linked_list_node *node;
-        ssize_t index = cx_linked_list_find_node(
-                (void **) &node,
-                ll->begin,
-                CX_LL_LOC_NEXT, CX_LL_LOC_DATA,
-                list->collection.cmpfunc, elem
-        );
-        if (node != NULL) {
-            cx_invoke_destructor(list, node->payload);
-            cx_linked_list_remove((void **) &ll->begin, (void **) &ll->end,
-                                  CX_LL_LOC_PREV, CX_LL_LOC_NEXT, node);
-            list->collection.size--;
-            cxFree(list->collection.allocator, node);
-        }
-        return index;
-    } else {
-        return cx_linked_list_find(
-                ((cx_linked_list *) list)->begin,
-                CX_LL_LOC_NEXT, CX_LL_LOC_DATA,
-                list->collection.cmpfunc, elem
-        );
+        cx_invoke_destructor(list, node->payload);
+        cx_linked_list_remove((void **) &ll->begin, (void **) &ll->end,
+                              CX_LL_LOC_PREV, CX_LL_LOC_NEXT, node);
+        list->collection.size--;
+        cxFree(list->collection.allocator, node);
     }
+    return index;
 }
 
 static void cx_ll_sort(struct cx_list_s *list) {
@@ -1138,17 +1105,8 @@
 
     cx_linked_list *list = cxCalloc(allocator, 1, sizeof(cx_linked_list));
     if (list == NULL) return NULL;
-
-    list->base.cl = &cx_linked_list_class;
-    list->base.collection.allocator = allocator;
-
-    if (elem_size > 0) {
-        list->base.collection.elem_size = elem_size;
-        list->base.collection.cmpfunc = comparator;
-    } else {
-        list->base.collection.cmpfunc = comparator == NULL ? cx_cmp_ptr : comparator;
-        cxListStorePointers((CxList *) list);
-    }
+    cx_list_init((CxList*)list, &cx_linked_list_class,
+            allocator, comparator, elem_size);
 
     return (CxList *) list;
 }
--- a/ucx/list.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/list.c	Tue Feb 25 21:12:11 2025 +0100
@@ -128,13 +128,13 @@
     return ptr == NULL ? NULL : *ptr;
 }
 
-static ssize_t cx_pl_find_remove(
+static size_t cx_pl_find_remove(
         struct cx_list_s *list,
         const void *elem,
         bool remove
 ) {
     cx_pl_hack_cmpfunc(list);
-    ssize_t ret = list->climpl->find_remove(list, &elem, remove);
+    size_t ret = list->climpl->find_remove(list, &elem, remove);
     cx_pl_unhack_cmpfunc(list);
     return ret;
 }
@@ -192,22 +192,6 @@
         cx_pl_reverse,
         cx_pl_iterator,
 };
-
-void cxListStoreObjects(CxList *list) {
-    list->collection.store_pointer = false;
-    if (list->climpl != NULL) {
-        list->cl = list->climpl;
-        list->climpl = NULL;
-    }
-}
-
-void cxListStorePointers(CxList *list) {
-    list->collection.elem_size = sizeof(void *);
-    list->collection.store_pointer = true;
-    list->climpl = list->cl;
-    list->cl = &cx_pointer_list_class;
-}
-
 // </editor-fold>
 
 // <editor-fold desc="empty list implementation">
@@ -223,12 +207,12 @@
     return NULL;
 }
 
-static ssize_t cx_emptyl_find_remove(
+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
 ) {
-    return -1;
+    return 0;
 }
 
 static bool cx_emptyl_iter_valid(cx_attr_unused const void *iter) {
@@ -265,18 +249,19 @@
 };
 
 CxList cx_empty_list = {
-        {
-                NULL,
-                NULL,
-                0,
-                0,
-                NULL,
-                NULL,
-                NULL,
-                false
-        },
-        &cx_empty_list_class,
-        NULL
+    {
+        NULL,
+        NULL,
+        0,
+        0,
+        NULL,
+        NULL,
+        NULL,
+        false,
+        true,
+    },
+    &cx_empty_list_class,
+    NULL
 };
 
 CxList *const cxEmptyList = &cx_empty_list;
@@ -417,6 +402,29 @@
     return 0;
 }
 
+void cx_list_init(
+    struct cx_list_s *list,
+    struct cx_list_class_s *cl,
+    const struct cx_allocator_s *allocator,
+    cx_compare_func comparator,
+    size_t elem_size
+) {
+    list->cl = cl;
+    list->collection.allocator = allocator;
+    list->collection.cmpfunc = comparator;
+    if (elem_size > 0) {
+        list->collection.elem_size = elem_size;
+    } else {
+        list->collection.elem_size = sizeof(void *);
+        if (list->collection.cmpfunc == NULL) {
+            list->collection.cmpfunc = cx_cmp_ptr;
+        }
+        list->collection.store_pointer = true;
+        list->climpl = list->cl;
+        list->cl = &cx_pointer_list_class;
+    }
+}
+
 int cxListCompare(
         const CxList *list,
         const CxList *other
@@ -481,3 +489,8 @@
     it.base.mutating = true;
     return it;
 }
+
+void cxListFree(CxList *list) {
+    if (list == NULL) return;
+    list->cl->deallocate(list);
+}
--- a/ucx/map.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/map.c	Tue Feb 25 21:12:11 2025 +0100
@@ -46,12 +46,12 @@
     return false;
 }
 
-static CxIterator cx_empty_map_iterator(
+static CxMapIterator cx_empty_map_iterator(
         const struct cx_map_s *map,
         cx_attr_unused enum cx_map_iterator_type type
 ) {
-    CxIterator iter = {0};
-    iter.src_handle.c = map;
+    CxMapIterator iter = {0};
+    iter.map.c = map;
     iter.base.valid = cx_empty_map_iter_valid;
     return iter;
 }
@@ -66,37 +66,43 @@
 };
 
 CxMap cx_empty_map = {
-        {
-                NULL,
-                NULL,
-                0,
-                0,
-                NULL,
-                NULL,
-                NULL,
-                false
-        },
-        &cx_empty_map_class
+    {
+        NULL,
+        NULL,
+        0,
+        0,
+        NULL,
+        NULL,
+        NULL,
+        false,
+        true
+    },
+    &cx_empty_map_class
 };
 
 CxMap *const cxEmptyMap = &cx_empty_map;
 
 // </editor-fold>
 
-CxIterator cxMapMutIteratorValues(CxMap *map) {
-    CxIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_VALUES);
+CxMapIterator cxMapMutIteratorValues(CxMap *map) {
+    CxMapIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_VALUES);
     it.base.mutating = true;
     return it;
 }
 
-CxIterator cxMapMutIteratorKeys(CxMap *map) {
-    CxIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_KEYS);
+CxMapIterator cxMapMutIteratorKeys(CxMap *map) {
+    CxMapIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_KEYS);
     it.base.mutating = true;
     return it;
 }
 
-CxIterator cxMapMutIterator(CxMap *map) {
-    CxIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_PAIRS);
+CxMapIterator cxMapMutIterator(CxMap *map) {
+    CxMapIterator it = map->cl->iterator(map, CX_MAP_ITERATOR_PAIRS);
     it.base.mutating = true;
     return it;
 }
+
+void cxMapFree(CxMap *map) {
+    if (map == NULL) return;
+    map->cl->deallocate(map);
+}
--- a/ucx/properties.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/properties.c	Tue Feb 25 21:12:11 2025 +0100
@@ -32,10 +32,10 @@
 
 const CxPropertiesConfig cx_properties_config_default = {
         '=',
-        //'\\',
         '#',
         '\0',
-        '\0'
+        '\0',
+    '\\',
 };
 
 void cxPropertiesInit(
@@ -226,8 +226,6 @@
                 return CX_PROPERTIES_INVALID_EMPTY_KEY;
             }
         }
-        // unreachable - either we returned or skipped a blank line
-        assert(false);
     }
 
     // when we come to this point, all data must have been read
@@ -254,7 +252,7 @@
 CxPropertiesSink cxPropertiesMapSink(CxMap *map) {
     CxPropertiesSink sink;
     sink.sink = map;
-    sink.data = cxDefaultAllocator;
+    sink.data = (void*) cxDefaultAllocator;
     sink.sink_func = cx_properties_sink_map;
     return sink;
 }
@@ -282,7 +280,7 @@
 ) {
     target->ptr = src->data_ptr;
     target->length = fread(src->data_ptr, 1, src->data_size, src->src);
-    return ferror(src->src);
+    return ferror((FILE*)src->src);
 }
 
 static int cx_properties_read_init_file(
@@ -362,6 +360,7 @@
 
     // transfer the data from the source to the sink
     CxPropertiesStatus status;
+    CxPropertiesStatus kv_status = CX_PROPERTIES_NO_DATA;
     bool found = false;
     while (true) {
         // read input
@@ -373,14 +372,23 @@
 
         // no more data - break
         if (input.length == 0) {
-            status = found ? CX_PROPERTIES_NO_ERROR : CX_PROPERTIES_NO_DATA;
+            if (found) {
+                // something was found, check the last kv_status
+                if (kv_status == CX_PROPERTIES_INCOMPLETE_DATA) {
+                    status = CX_PROPERTIES_INCOMPLETE_DATA;
+                } else {
+                    status = CX_PROPERTIES_NO_ERROR;
+                }
+            } else {
+                // nothing found
+                status = CX_PROPERTIES_NO_DATA;
+            }
             break;
         }
 
         // set the input buffer and read the k/v-pairs
         cxPropertiesFill(prop, input);
 
-        CxPropertiesStatus kv_status;
         do {
             cxstring key, value;
             kv_status = cxPropertiesNext(prop, &key, &value);
--- a/ucx/string.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/string.c	Tue Feb 25 21:12:11 2025 +0100
@@ -25,12 +25,10 @@
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  * POSSIBILITY OF SUCH DAMAGE.
  */
-#define CX_STR_IMPLEMENTATION
 #include "cx/string.h"
 
 #include <string.h>
 #include <stdarg.h>
-#include <ctype.h>
 #include <assert.h>
 #include <errno.h>
 #include <limits.h>
@@ -222,14 +220,9 @@
         cxstring string,
         int chr
 ) {
-    chr = 0xFF & chr;
-    // TODO: improve by comparing multiple bytes at once
-    for (size_t i = 0; i < string.length; i++) {
-        if (string.ptr[i] == chr) {
-            return cx_strsubs(string, i);
-        }
-    }
-    return (cxstring) {NULL, 0};
+    char *ret = memchr(string.ptr, 0xFF & chr, string.length);
+    if (ret == NULL) return (cxstring) {NULL, 0};
+    return (cxstring) {ret, string.length - (ret - string.ptr)};
 }
 
 cxmutstr cx_strchr_m(
@@ -265,7 +258,7 @@
 }
 
 #ifndef CX_STRSTR_SBO_SIZE
-#define CX_STRSTR_SBO_SIZE 512
+#define CX_STRSTR_SBO_SIZE 128
 #endif
 const unsigned cx_strstr_sbo_size = CX_STRSTR_SBO_SIZE;
 
@@ -295,7 +288,7 @@
 
     // check needle length and use appropriate prefix table
     // if the pattern exceeds static prefix table, allocate on the heap
-    bool useheap = needle.length >= CX_STRSTR_SBO_SIZE;
+    const bool useheap = needle.length >= CX_STRSTR_SBO_SIZE;
     register size_t *ptable = useheap ? calloc(needle.length + 1,
                                                sizeof(size_t)) : s_prefix_table;
 
@@ -334,7 +327,7 @@
     }
 
     // if prefix table was allocated on the heap, free it
-    if (ptable != s_prefix_table) {
+    if (useheap) {
         free(ptable);
     }
 
@@ -512,7 +505,7 @@
     return cx_strcasecmp(*left, *right);
 }
 
-cxmutstr cx_strdup_a(
+cxmutstr cx_strdup_a_(
         const CxAllocator *allocator,
         cxstring string
 ) {
@@ -529,14 +522,19 @@
     return result;
 }
 
+static bool str_isspace(char c) {
+    // TODO: remove once UCX has public API for this
+    return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\v' || c == '\f';
+}
+
 cxstring cx_strtrim(cxstring string) {
     cxstring result = string;
     // TODO: optimize by comparing multiple bytes at once
-    while (result.length > 0 && isspace(*result.ptr)) {
+    while (result.length > 0 && str_isspace(*result.ptr)) {
         result.ptr++;
         result.length--;
     }
-    while (result.length > 0 && isspace(result.ptr[result.length - 1])) {
+    while (result.length > 0 && str_isspace(result.ptr[result.length - 1])) {
         result.length--;
     }
     return result;
@@ -590,18 +588,6 @@
 #endif
 }
 
-void cx_strlower(cxmutstr string) {
-    for (size_t i = 0; i < string.length; i++) {
-        string.ptr[i] = (char) tolower(string.ptr[i]);
-    }
-}
-
-void cx_strupper(cxmutstr string) {
-    for (size_t i = 0; i < string.length; i++) {
-        string.ptr[i] = (char) toupper(string.ptr[i]);
-    }
-}
-
 #ifndef CX_STRREPLACE_INDEX_BUFFER_SIZE
 #define CX_STRREPLACE_INDEX_BUFFER_SIZE 64
 #endif
@@ -613,6 +599,8 @@
 };
 
 static void cx_strrepl_free_ibuf(struct cx_strreplace_ibuf *buf) {
+    // remember, the first data is on the stack!
+    buf = buf->next;
     while (buf) {
         struct cx_strreplace_ibuf *next = buf->next;
         free(buf->buf);
@@ -624,49 +612,46 @@
 cxmutstr cx_strreplacen_a(
         const CxAllocator *allocator,
         cxstring str,
-        cxstring pattern,
+        cxstring search,
         cxstring replacement,
         size_t replmax
 ) {
 
-    if (pattern.length == 0 || pattern.length > str.length || replmax == 0)
+    if (search.length == 0 || search.length > str.length || replmax == 0)
         return cx_strdup_a(allocator, str);
 
     // Compute expected buffer length
-    size_t ibufmax = str.length / pattern.length;
+    size_t ibufmax = str.length / search.length;
     size_t ibuflen = replmax < ibufmax ? replmax : ibufmax;
     if (ibuflen > CX_STRREPLACE_INDEX_BUFFER_SIZE) {
         ibuflen = CX_STRREPLACE_INDEX_BUFFER_SIZE;
     }
 
-    // Allocate first index buffer
-    struct cx_strreplace_ibuf *firstbuf, *curbuf;
-    firstbuf = curbuf = calloc(1, sizeof(struct cx_strreplace_ibuf));
-    if (!firstbuf) return cx_mutstrn(NULL, 0);
-    firstbuf->buf = calloc(ibuflen, sizeof(size_t));
-    if (!firstbuf->buf) {
-        free(firstbuf);
-        return cx_mutstrn(NULL, 0);
-    }
+    // First index buffer can be on the stack
+    struct cx_strreplace_ibuf ibuf, *curbuf = &ibuf;
+    size_t ibuf_sbo[CX_STRREPLACE_INDEX_BUFFER_SIZE];
+    ibuf.buf = ibuf_sbo;
+    ibuf.next = NULL;
+    ibuf.len = 0;
 
     // Search occurrences
     cxstring searchstr = str;
     size_t found = 0;
     do {
-        cxstring match = cx_strstr(searchstr, pattern);
+        cxstring match = cx_strstr(searchstr, search);
         if (match.length > 0) {
             // Allocate next buffer in chain, if required
             if (curbuf->len == ibuflen) {
                 struct cx_strreplace_ibuf *nextbuf =
                         calloc(1, sizeof(struct cx_strreplace_ibuf));
                 if (!nextbuf) {
-                    cx_strrepl_free_ibuf(firstbuf);
+                    cx_strrepl_free_ibuf(&ibuf);
                     return cx_mutstrn(NULL, 0);
                 }
                 nextbuf->buf = calloc(ibuflen, sizeof(size_t));
                 if (!nextbuf->buf) {
                     free(nextbuf);
-                    cx_strrepl_free_ibuf(firstbuf);
+                    cx_strrepl_free_ibuf(&ibuf);
                     return cx_mutstrn(NULL, 0);
                 }
                 curbuf->next = nextbuf;
@@ -677,8 +662,8 @@
             found++;
             size_t idx = match.ptr - str.ptr;
             curbuf->buf[curbuf->len++] = idx;
-            searchstr.ptr = match.ptr + pattern.length;
-            searchstr.length = str.length - idx - pattern.length;
+            searchstr.ptr = match.ptr + search.length;
+            searchstr.length = str.length - idx - search.length;
         } else {
             break;
         }
@@ -687,9 +672,9 @@
     // Allocate result string
     cxmutstr result;
     {
-        ssize_t adjlen = (ssize_t) replacement.length - (ssize_t) pattern.length;
+        long long adjlen = (long long) replacement.length - (long long) search.length;
         size_t rcount = 0;
-        curbuf = firstbuf;
+        curbuf = &ibuf;
         do {
             rcount += curbuf->len;
             curbuf = curbuf->next;
@@ -697,13 +682,13 @@
         result.length = str.length + rcount * adjlen;
         result.ptr = cxMalloc(allocator, result.length + 1);
         if (!result.ptr) {
-            cx_strrepl_free_ibuf(firstbuf);
+            cx_strrepl_free_ibuf(&ibuf);
             return cx_mutstrn(NULL, 0);
         }
     }
 
     // Build result string
-    curbuf = firstbuf;
+    curbuf = &ibuf;
     size_t srcidx = 0;
     char *destptr = result.ptr;
     do {
@@ -718,7 +703,7 @@
             }
 
             // Copy the replacement and skip the source pattern
-            srcidx += pattern.length;
+            srcidx += search.length;
             memcpy(destptr, replacement.ptr, replacement.length);
             destptr += replacement.length;
         }
@@ -730,12 +715,12 @@
     result.ptr[result.length] = '\0';
 
     // Free index buffer
-    cx_strrepl_free_ibuf(firstbuf);
+    cx_strrepl_free_ibuf(&ibuf);
 
     return result;
 }
 
-CxStrtokCtx cx_strtok(
+CxStrtokCtx cx_strtok_(
         cxstring str,
         cxstring delim,
         size_t limit
@@ -753,14 +738,6 @@
     return ctx;
 }
 
-CxStrtokCtx cx_strtok_m(
-        cxmutstr str,
-        cxstring delim,
-        size_t limit
-) {
-    return cx_strtok(cx_strcast(str), delim, limit);
-}
-
 bool cx_strtok_next(
         CxStrtokCtx *ctx,
         cxstring *token
@@ -832,25 +809,24 @@
     *output = (rtype) result; \
     return 0
 
-int cx_strtos_lc(cxstring str, short *output, int base, const char *groupsep) {
+int cx_strtos_lc_(cxstring str, short *output, int base, const char *groupsep) {
     cx_strtoX_signed_impl(short, SHRT_MIN, SHRT_MAX);
 }
 
-int cx_strtoi_lc(cxstring str, int *output, int base, const char *groupsep) {
+int cx_strtoi_lc_(cxstring str, int *output, int base, const char *groupsep) {
     cx_strtoX_signed_impl(int, INT_MIN, INT_MAX);
 }
 
-int cx_strtol_lc(cxstring str, long *output, int base, const char *groupsep) {
+int cx_strtol_lc_(cxstring str, long *output, int base, const char *groupsep) {
     cx_strtoX_signed_impl(long, LONG_MIN, LONG_MAX);
 }
 
-int cx_strtoll_lc(cxstring str, long long *output, int base, const char *groupsep) {
+int cx_strtoll_lc_(cxstring str, long long *output, int base, const char *groupsep) {
     // strategy: parse as unsigned, check range, negate if required
     bool neg = false;
     size_t start_unsigned = 0;
 
-    // trim already, to search for a sign character
-    str = cx_strtrim(str);
+    // emptiness check
     if (str.length == 0) {
         errno = EINVAL;
         return -1;
@@ -890,33 +866,23 @@
     }
 }
 
-int cx_strtoi8_lc(cxstring str, int8_t *output, int base, const char *groupsep) {
+int cx_strtoi8_lc_(cxstring str, int8_t *output, int base, const char *groupsep) {
     cx_strtoX_signed_impl(int8_t, INT8_MIN, INT8_MAX);
 }
 
-int cx_strtoi16_lc(cxstring str, int16_t *output, int base, const char *groupsep) {
+int cx_strtoi16_lc_(cxstring str, int16_t *output, int base, const char *groupsep) {
     cx_strtoX_signed_impl(int16_t, INT16_MIN, INT16_MAX);
 }
 
-int cx_strtoi32_lc(cxstring str, int32_t *output, int base, const char *groupsep) {
+int cx_strtoi32_lc_(cxstring str, int32_t *output, int base, const char *groupsep) {
     cx_strtoX_signed_impl(int32_t, INT32_MIN, INT32_MAX);
 }
 
-int cx_strtoi64_lc(cxstring str, int64_t *output, int base, const char *groupsep) {
+int cx_strtoi64_lc_(cxstring str, int64_t *output, int base, const char *groupsep) {
     assert(sizeof(long long) == sizeof(int64_t)); // should be true on all platforms
     return cx_strtoll_lc(str, (long long*) output, base, groupsep);
 }
 
-int cx_strtoz_lc(cxstring str, ssize_t *output, int base, const char *groupsep) {
-#if SSIZE_MAX == INT32_MAX
-    return cx_strtoi32_lc(str, (int32_t*) output, base, groupsep);
-#elif SSIZE_MAX == INT64_MAX
-    return cx_strtoll_lc(str, (long long*) output, base, groupsep);
-#else
-#error "unsupported ssize_t size"
-#endif
-}
-
 #define cx_strtoX_unsigned_impl(rtype, rmax) \
     uint64_t result; \
     if (cx_strtou64_lc(str, &result, base, groupsep)) { \
@@ -929,21 +895,20 @@
     *output = (rtype) result; \
     return 0
 
-int cx_strtous_lc(cxstring str, unsigned short *output, int base, const char *groupsep) {
+int cx_strtous_lc_(cxstring str, unsigned short *output, int base, const char *groupsep) {
     cx_strtoX_unsigned_impl(unsigned short, USHRT_MAX);
 }
 
-int cx_strtou_lc(cxstring str, unsigned int *output, int base, const char *groupsep) {
+int cx_strtou_lc_(cxstring str, unsigned int *output, int base, const char *groupsep) {
     cx_strtoX_unsigned_impl(unsigned int, UINT_MAX);
 }
 
-int cx_strtoul_lc(cxstring str, unsigned long *output, int base, const char *groupsep) {
+int cx_strtoul_lc_(cxstring str, unsigned long *output, int base, const char *groupsep) {
     cx_strtoX_unsigned_impl(unsigned long, ULONG_MAX);
 }
 
-int cx_strtoull_lc(cxstring str, unsigned long long *output, int base, const char *groupsep) {
+int cx_strtoull_lc_(cxstring str, unsigned long long *output, int base, const char *groupsep) {
     // some sanity checks
-    str = cx_strtrim(str);
     if (str.length == 0) {
         errno = EINVAL;
         return -1;
@@ -1021,37 +986,37 @@
     return 0;
 }
 
-int cx_strtou8_lc(cxstring str, uint8_t *output, int base, const char *groupsep) {
+int cx_strtou8_lc_(cxstring str, uint8_t *output, int base, const char *groupsep) {
     cx_strtoX_unsigned_impl(uint8_t, UINT8_MAX);
 }
 
-int cx_strtou16_lc(cxstring str, uint16_t *output, int base, const char *groupsep) {
+int cx_strtou16_lc_(cxstring str, uint16_t *output, int base, const char *groupsep) {
     cx_strtoX_unsigned_impl(uint16_t, UINT16_MAX);
 }
 
-int cx_strtou32_lc(cxstring str, uint32_t *output, int base, const char *groupsep) {
+int cx_strtou32_lc_(cxstring str, uint32_t *output, int base, const char *groupsep) {
     cx_strtoX_unsigned_impl(uint32_t, UINT32_MAX);
 }
 
-int cx_strtou64_lc(cxstring str, uint64_t *output, int base, const char *groupsep) {
+int cx_strtou64_lc_(cxstring str, uint64_t *output, int base, const char *groupsep) {
     assert(sizeof(unsigned long long) == sizeof(uint64_t)); // should be true on all platforms
     return cx_strtoull_lc(str, (unsigned long long*) output, base, groupsep);
 }
 
-int cx_strtouz_lc(cxstring str, size_t *output, int base, const char *groupsep) {
+int cx_strtoz_lc_(cxstring str, size_t *output, int base, const char *groupsep) {
 #if SIZE_MAX == UINT32_MAX
-    return cx_strtou32_lc(str, (uint32_t*) output, base, groupsep);
+    return cx_strtou32_lc_(str, (uint32_t*) output, base, groupsep);
 #elif SIZE_MAX == UINT64_MAX
-    return cx_strtoull_lc(str, (unsigned long long *) output, base, groupsep);
+    return cx_strtoull_lc_(str, (unsigned long long *) output, base, groupsep);
 #else
 #error "unsupported size_t size"
 #endif
 }
 
-int cx_strtof_lc(cxstring str, float *output, char decsep, const char *groupsep) {
+int cx_strtof_lc_(cxstring str, float *output, char decsep, const char *groupsep) {
     // use string to double and add a range check
     double d;
-    int ret = cx_strtod_lc(str, &d, decsep, groupsep);
+    int ret = cx_strtod_lc_(str, &d, decsep, groupsep);
     if (ret != 0) return ret;
     // note: FLT_MIN is the smallest POSITIVE number that can be represented
     double test = d < 0 ? -d : d;
@@ -1063,12 +1028,16 @@
     return 0;
 }
 
-int cx_strtod_lc(cxstring str, double *output, char decsep, const char *groupsep) {
+static bool str_isdigit(char c) {
+    // TODO: remove once UCX has public API for this
+    return c >= '0' && c <= '9';
+}
+
+int cx_strtod_lc_(cxstring str, double *output, char decsep, const char *groupsep) {
     // TODO: overflow check
     // TODO: increase precision
 
-    // trim and check
-    str = cx_strtrim(str);
+    // emptiness check
     if (str.length == 0) {
         errno = EINVAL;
         return -1;
@@ -1096,7 +1065,7 @@
     // parse all digits until we find the decsep
     size_t pos = 0;
     do {
-        if (isdigit(str.ptr[pos])) {
+        if (str_isdigit(str.ptr[pos])) {
             result = result * 10 + (str.ptr[pos] - '0');
         } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
             break;
@@ -1125,7 +1094,7 @@
         // parse everything until exponent or end
         double factor = 1.;
         do {
-            if (isdigit(str.ptr[pos])) {
+            if (str_isdigit(str.ptr[pos])) {
                 factor *= 0.1;
                 result = result + factor * (str.ptr[pos] - '0');
             } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
@@ -1166,7 +1135,7 @@
     // parse the exponent
     unsigned int exp = 0;
     do {
-        if (isdigit(str.ptr[pos])) {
+        if (str_isdigit(str.ptr[pos])) {
             exp = 10 * exp + (str.ptr[pos] - '0');
         } else if (strchr(groupsep, str.ptr[pos]) == NULL) {
             errno = EINVAL;
--- a/ucx/tree.c	Sun Feb 16 17:38:07 2025 +0100
+++ b/ucx/tree.c	Tue Feb 25 21:12:11 2025 +0100
@@ -42,6 +42,13 @@
 #define cx_tree_ptr_locations \
     loc_parent, loc_children, loc_last_child, loc_prev, loc_next
 
+#define cx_tree_node_layout(tree) \
+    (tree)->loc_parent,\
+    (tree)->loc_children,\
+    (tree)->loc_last_child,\
+    (tree)->loc_prev,  \
+    (tree)->loc_next
+
 static void cx_tree_zero_pointers(
         void *node,
         ptrdiff_t loc_parent,
@@ -742,7 +749,7 @@
 
 static size_t cx_tree_default_insert_many(
         CxTree *tree,
-        struct cx_iterator_base_s *iter,
+        CxIteratorBase *iter,
         size_t n
 ) {
     size_t ins = 0;
@@ -837,6 +844,14 @@
     return tree;
 }
 
+void cxTreeFree(CxTree *tree) {
+    if (tree == NULL) return;
+    if (tree->root != NULL) {
+        cxTreeClear(tree);
+    }
+    cxFree(tree->allocator, tree);
+}
+
 CxTree *cxTreeCreateWrapped(
         const CxAllocator *allocator,
         void *root,

mercurial