update libs (ucx, toolkit, libidav) default tip

2 weeks ago

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Tue, 25 Feb 2025 21:11:00 +0100 (2 weeks ago)
changeset 102
64ded9f6a6c6
parent 101
7b3a3130be44

update libs (ucx, toolkit, libidav)

application/application.h file | annotate | diff | comparison | revisions
application/xml.c file | annotate | diff | comparison | revisions
libidav/config.c file | annotate | diff | comparison | revisions
libidav/davqlexec.c file | annotate | diff | comparison | revisions
libidav/methods.c file | annotate | diff | comparison | revisions
libidav/pwdstore.c file | annotate | diff | comparison | revisions
libidav/resource.c file | annotate | diff | comparison | revisions
libidav/session.c file | annotate | diff | comparison | revisions
libidav/utils.c file | annotate | diff | comparison | revisions
libidav/webdav.c file | annotate | diff | comparison | revisions
ucx/allocator.c file | annotate | diff | comparison | revisions
ucx/array_list.c file | annotate | diff | comparison | revisions
ucx/buffer.c file | annotate | diff | comparison | revisions
ucx/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/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
ui/cocoa/EventData.h file | annotate | diff | comparison | revisions
ui/cocoa/EventData.m file | annotate | diff | comparison | revisions
ui/cocoa/GridLayout.h file | annotate | diff | comparison | revisions
ui/cocoa/GridLayout.m file | annotate | diff | comparison | revisions
ui/cocoa/MainWindow.m file | annotate | diff | comparison | revisions
ui/cocoa/button.h file | annotate | diff | comparison | revisions
ui/cocoa/button.m file | annotate | diff | comparison | revisions
ui/cocoa/window.m file | annotate | diff | comparison | revisions
ui/common/context.c file | annotate | diff | comparison | revisions
ui/common/document.c file | annotate | diff | comparison | revisions
ui/common/types.c file | annotate | diff | comparison | revisions
ui/common/ucx_properties.c file | annotate | diff | comparison | revisions
ui/gtk/container.c file | annotate | diff | comparison | revisions
ui/gtk/container.h file | annotate | diff | comparison | revisions
ui/gtk/list.c file | annotate | diff | comparison | revisions
ui/gtk/list.h file | annotate | diff | comparison | revisions
ui/gtk/objs.mk file | annotate | diff | comparison | revisions
ui/gtk/webview.c file | annotate | diff | comparison | revisions
ui/gtk/webview.h file | annotate | diff | comparison | revisions
ui/gtk/window.c file | annotate | diff | comparison | revisions
ui/motif/Grid.c file | annotate | diff | comparison | revisions
ui/motif/container.c file | annotate | diff | comparison | revisions
ui/motif/container.h file | annotate | diff | comparison | revisions
ui/motif/list.c file | annotate | diff | comparison | revisions
ui/motif/text.c file | annotate | diff | comparison | revisions
ui/motif/text.h file | annotate | diff | comparison | revisions
ui/motif/toolkit.c file | annotate | diff | comparison | revisions
ui/motif/window.c file | annotate | diff | comparison | revisions
ui/ui/button.h file | annotate | diff | comparison | revisions
ui/ui/container.h file | annotate | diff | comparison | revisions
ui/ui/display.h file | annotate | diff | comparison | revisions
ui/ui/entry.h file | annotate | diff | comparison | revisions
ui/ui/image.h file | annotate | diff | comparison | revisions
ui/ui/range.h file | annotate | diff | comparison | revisions
ui/ui/text.h file | annotate | diff | comparison | revisions
ui/ui/toolkit.h file | annotate | diff | comparison | revisions
ui/ui/tree.h file | annotate | diff | comparison | revisions
ui/ui/ui.h file | annotate | diff | comparison | revisions
ui/ui/webview.h file | annotate | diff | comparison | revisions
ui/winui/window.cpp file | annotate | diff | comparison | revisions
--- a/application/application.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/application/application.h	Tue Feb 25 21:11:00 2025 +0100
@@ -43,7 +43,7 @@
 #ifdef    __cplusplus
 extern "C" {
 #endif
-
+   
 #define APP_STATE_BROWSER_SESSION 100
 #define APP_STATE_BROWSER_SELECTION 110
     
--- a/application/xml.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/application/xml.c	Tue Feb 25 21:11:00 2025 +0100
@@ -55,7 +55,7 @@
     
     CxBuffer nsbuf;
     cxBufferInit(&nsbuf, NULL, 2048, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND);
-    CxIterator i = cxMapIterator(nsmap);
+    CxMapIterator i = cxMapIterator(nsmap);
     int addSpace = 0;
     cx_foreach(CxMapEntry *, entry, i) {
         const char *ns = entry->key->data;
--- a/libidav/config.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/libidav/config.c	Tue Feb 25 21:11:00 2025 +0100
@@ -1002,7 +1002,7 @@
 
 int dav_config_register_keys(DavConfig *config, DavContext *ctx, dav_loadkeyfile_func loadkey) {
     for(DavCfgKey *key=config->keys;key;key=key->next) {
-        char *file = cx_strdup_m(key->file.value).ptr;
+        char *file = cx_strdup_a(cxDefaultAllocator, key->file.value).ptr;
         cxmutstr keycontent = loadkey(file);
         free(file);
         
@@ -1013,7 +1013,7 @@
         }
         
         DavKey *davkey = calloc(1, sizeof(DavKey));
-        davkey->name = cx_strdup_m(key->name.value).ptr;
+        davkey->name = cx_strdup_a(cxDefaultAllocator, key->name.value).ptr;
         davkey->type = dav_config_keytype(key->type);
         davkey->data = keycontent.ptr;
         davkey->length = keycontent.length;
--- a/libidav/davqlexec.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/libidav/davqlexec.c	Tue Feb 25 21:11:00 2025 +0100
@@ -302,9 +302,9 @@
         }
     }
     
-    i = cxMapIteratorValues(properties);
+    CxMapIterator mi = cxMapIteratorValues(properties);
     CxList *list = cxLinkedListCreateSimple(CX_STORE_POINTERS);
-    cx_foreach(DavProperty*, value, i) {
+    cx_foreach(DavProperty*, value, mi) {
         cxListAdd(list, value);
     }
     
@@ -464,7 +464,7 @@
  * execute a davql select statement
  */
 DavResult dav_exec_select(DavSession *sn, DavQLStatement *st, va_list ap) {
-    CxMempool *mp = cxBasicMempoolCreate(128);
+    CxMempool *mp = cxMempoolCreateSimple(128);
     DavResult result;
     result.result = NULL;
     result.status = 1;
--- a/libidav/methods.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/libidav/methods.c	Tue Feb 25 21:11:00 2025 +0100
@@ -200,7 +200,7 @@
     // write root element and namespaces
     cx_bprintf(buf, "<D:%s xmlns:D=\"DAV:\"", rootelm);
     
-    CxIterator mapi = cxMapIteratorValues(namespaces);
+    CxMapIterator mapi = cxMapIteratorValues(namespaces);
     cx_foreach(DavNamespace*, ns, mapi) {
         s = CX_STR(" xmlns:");
         cxBufferWrite(s.ptr, 1, s.length, buf);
@@ -862,7 +862,7 @@
     // write root element and namespaces
     s = CX_STR("<D:propertyupdate xmlns:D=\"DAV:\"");
     cxBufferWrite(s.ptr, 1, s.length, buf);
-    CxIterator mapi = cxMapIterator(namespaces);
+    CxMapIterator mapi = cxMapIterator(namespaces);
     cx_foreach(CxMapEntry*, entry, mapi) {
         s = CX_STR(" xmlns:");
         cxBufferWrite(s.ptr, 1, s.length, buf);
--- a/libidav/pwdstore.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/libidav/pwdstore.c	Tue Feb 25 21:11:00 2025 +0100
@@ -137,7 +137,7 @@
     newp->encoffset = p->encoffset;
     newp->isdecrypted = p->isdecrypted;
     
-    CxIterator i = cxMapIterator(p->ids);
+    CxMapIterator i = cxMapIterator(p->ids);
     cx_foreach(CxMapEntry *, e, i) {
         PwdEntry *entry = e->value;
         pwdstore_put(newp, entry->id, entry->user, entry->password);
@@ -489,8 +489,8 @@
         write_index_entry(index, e);
     }
     
-    i = cxMapIteratorValues(p->ids);
-    cx_foreach(PwdEntry*, value, i) {
+    CxMapIterator mi = cxMapIteratorValues(p->ids);
+    cx_foreach(PwdEntry*, value, mi) {
         if(!value->id || !value->user || !value->password) {
             continue;
         }
--- a/libidav/resource.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/libidav/resource.c	Tue Feb 25 21:11:00 2025 +0100
@@ -126,7 +126,7 @@
 void resource_free_properties(DavSession *sn, CxMap *properties) {
     if(!properties) return;
     
-    CxIterator i = cxMapIteratorValues(properties);
+    CxMapIterator i = cxMapIteratorValues(properties);
     cx_foreach(DavProperty*, property, i) {
         // TODO: free everything
         dav_session_free(sn, property);
@@ -741,7 +741,7 @@
             sizeof(DavPropName));
     
     
-    CxIterator i = cxMapIteratorValues(data->properties);
+    CxMapIterator i = cxMapIteratorValues(data->properties);
     cx_foreach(DavProperty*, value, i) {
         DavPropName *name = &names[i.index];
         name->ns = value->ns->name;
@@ -1515,7 +1515,7 @@
     cxBufferPutString(content, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
     cxBufferPutString(content, "<D:prop xmlns:D=\"DAV:\">\n");
     
-    CxIterator i = cxMapIteratorValues(properties);
+    CxMapIterator i = cxMapIteratorValues(properties);
     cx_foreach(DavProperty*, prop, i) {
         DavXmlNode pnode;
         pnode.type = DAV_XML_ELEMENT;
--- a/libidav/session.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/libidav/session.c	Tue Feb 25 21:11:00 2025 +0100
@@ -49,7 +49,7 @@
     }
     DavSession *sn = malloc(sizeof(DavSession));
     memset(sn, 0, sizeof(DavSession));
-    sn->mp = cxBasicMempoolCreate(DAV_SESSION_MEMPOOL_SIZE);
+    sn->mp = cxMempoolCreateSimple(DAV_SESSION_MEMPOOL_SIZE);
     sn->pathcache = cxHashMapCreate(sn->mp->allocator, CX_STORE_POINTERS, DAV_PATH_CACHE_SIZE);
     sn->key = NULL;
     sn->errorstr = NULL;
@@ -118,7 +118,7 @@
 
     DavSession *newsn = malloc(sizeof(DavSession));
     memset(newsn, 0, sizeof(DavSession));
-    newsn->mp = cxBasicMempoolCreate(DAV_SESSION_MEMPOOL_SIZE);
+    newsn->mp = cxMempoolCreateSimple(DAV_SESSION_MEMPOOL_SIZE);
     newsn->pathcache = cxHashMapCreate(newsn->mp->allocator, CX_STORE_POINTERS, DAV_PATH_CACHE_SIZE);
     newsn->key = sn->key;
     newsn->errorstr = NULL;
--- a/libidav/utils.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/libidav/utils.c	Tue Feb 25 21:11:00 2025 +0100
@@ -385,7 +385,9 @@
     value.ptr++; value.length--;
     
     cxmutstr key_cp = cx_strdup(cx_strtrim(key));
-    cx_strlower(key_cp);
+    for(int i=0;i<key_cp.length;i++) {
+        key_cp.ptr[i] = tolower(key_cp.ptr[i]);
+    }
     cxmutstr value_cp = cx_strdup(cx_strtrim(value));
         
     cxMapPut(map, cx_hash_key(key_cp.ptr, key_cp.length), value_cp.ptr);
--- a/libidav/webdav.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/libidav/webdav.c	Tue Feb 25 21:11:00 2025 +0100
@@ -112,7 +112,7 @@
     }
     
     if(ctx->namespaces) {
-        CxIterator i = cxMapIteratorValues(ctx->namespaces);
+        CxMapIterator i = cxMapIteratorValues(ctx->namespaces);
         cx_foreach(DavNamespace*, ns, i) {
             if(!ns) continue;
             if(ns->prefix) {
@@ -129,7 +129,7 @@
         // TODO: implement
     }
     if(ctx->keys) {
-        CxIterator i = cxMapIteratorValues(ctx->keys);
+        CxMapIterator i = cxMapIteratorValues(ctx->keys);
         cx_foreach(DavKey*, key, i) {
             if(!key) continue;
             if(key->name) {
--- a/ucx/allocator.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/allocator.c	Tue Feb 25 21:11:00 2025 +0100
@@ -47,10 +47,10 @@
 
 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);
 }
 
 static void cx_free_stdlib(
@@ -71,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
 ) {
@@ -87,8 +86,7 @@
     }
 }
 
-#undef cx_reallocatearray
-int cx_reallocatearray(
+int cx_reallocatearray_(
         void **mem,
         size_t nmemb,
         size_t size
@@ -140,8 +138,7 @@
     }
 }
 
-#undef cxReallocate
-int cxReallocate(
+int cxReallocate_(
         const CxAllocator *allocator,
         void **mem,
         size_t n
@@ -155,8 +152,7 @@
     }
 }
 
-#undef cxReallocateArray
-int cxReallocateArray(
+int cxReallocateArray_(
         const CxAllocator *allocator,
         void **mem,
         size_t nmemb,
@@ -173,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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/array_list.c	Tue Feb 25 21:11:00 2025 +0100
@@ -856,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) {
@@ -1013,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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/buffer.c	Tue Feb 25 21:11:00 2025 +0100
@@ -205,8 +205,8 @@
 
 static size_t cx_buffer_flush_helper(
         const CxBuffer *buffer,
+        const unsigned char *src,
         size_t size,
-        const unsigned char *src,
         size_t nitems
 ) {
     // flush data from an arbitrary source
@@ -236,7 +236,7 @@
     unsigned char *space = buffer->bytes;
     size_t remaining = buffer->pos / size;
     size_t flushed_total = cx_buffer_flush_helper(
-        buffer, size, space, remaining);
+        buffer, space, size, remaining);
 
     // shift the buffer left after flushing
     // IMPORTANT: up to this point, copy on write must have been
@@ -268,17 +268,18 @@
         return nitems;
     }
 
-    size_t len;
+    size_t len, total_flushed = 0;
+cx_buffer_write_retry:
     if (cx_szmul(size, nitems, &len)) {
         errno = EOVERFLOW;
-        return 0;
+        return total_flushed;
     }
     if (buffer->pos > SIZE_MAX - len) {
         errno = EOVERFLOW;
-        return 0;
+        return total_flushed;
     }
+
     size_t required = buffer->pos + len;
-
     bool perform_flush = false;
     if (required > buffer->capacity) {
         if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
@@ -286,7 +287,7 @@
                 perform_flush = true;
             } else {
                 if (cxBufferMinimumCapacity(buffer, required)) {
-                    return 0; // LCOV_EXCL_LINE
+                    return total_flushed; // LCOV_EXCL_LINE
                 }
             }
         } else {
@@ -305,7 +306,7 @@
 
     // check here and not above because of possible truncation
     if (len == 0) {
-        return 0;
+        return total_flushed;
     }
 
     // check if we need to copy
@@ -313,26 +314,43 @@
 
     // perform the operation
     if (perform_flush) {
-        size_t items_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_flush = cx_buffer_flush_helper(buffer, size, ptr, nitems);
-            if (items_flush == 0) {
-                // we needed to flush, but could not flush anything
-                // give up and avoid endless trying
+            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;
             }
-            size_t ritems = nitems - items_flush;
-            const unsigned char *rest = ptr;
-            rest += items_flush * size;
-            return items_flush + cxBufferWrite(rest, size, ritems, buffer);
+            nitems -= items_flushed;
+            total_flushed += items_flushed;
+            if (nitems > 0) {
+                ptr = ((unsigned char*)ptr) + items_flushed * size;
+                goto cx_buffer_write_retry;
+            }
+            return total_flushed;
         } else {
-            items_flush = cx_buffer_flush_impl(buffer, size);
-            if (items_flush == 0) {
-                return 0;
+            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;
+                }
             }
-            return cxBufferWrite(ptr, size, nitems, buffer);
+            goto cx_buffer_write_retry;
         }
     } else {
         memcpy(buffer->bytes + buffer->pos, ptr, len);
@@ -340,9 +358,8 @@
         if (buffer->pos > buffer->size) {
             buffer->size = buffer->pos;
         }
-        return nitems;
+        return total_flushed + nitems;
     }
-
 }
 
 size_t cxBufferAppend(
@@ -352,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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/allocator.h	Tue Feb 25 21:11:00 2025 +0100
@@ -65,8 +65,8 @@
      */
     void *(*calloc)(
             void *data,
-            size_t nelem,
-            size_t n
+            size_t nmemb,
+            size_t size
     );
 
     /**
@@ -100,7 +100,8 @@
 /**
  * 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.
@@ -131,7 +132,7 @@
 );
 
 /**
- * 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
@@ -145,13 +146,14 @@
  */
 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.
@@ -169,14 +171,15 @@
  */
 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
@@ -188,10 +191,10 @@
  * @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.
@@ -207,7 +210,7 @@
  * @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.
@@ -218,6 +221,7 @@
  * @param mem a pointer to the block to free
  */
 cx_attr_nonnull_arg(1)
+cx_attr_export
 void cxFree(
         const CxAllocator *allocator,
         void *mem
@@ -235,13 +239,14 @@
 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
+ * 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.
@@ -251,12 +256,13 @@
  * @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,7 +270,7 @@
 );
 
 /**
- * Re-allocate the previously allocated block in @p mem, making the new block
+ * 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.
@@ -279,12 +285,13 @@
  * @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,7 +300,7 @@
 );
 
 /**
- * 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.
  *
@@ -310,14 +317,15 @@
  */
 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.
  *
@@ -333,10 +341,10 @@
  * @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.
@@ -356,7 +364,8 @@
  */
 cx_attr_nodiscard
 cx_attr_nonnull
-int cxReallocateArray(
+cx_attr_export
+int cxReallocateArray_(
         const CxAllocator *allocator,
         void **mem,
         size_t nmemb,
@@ -364,7 +373,7 @@
 );
 
 /**
- * 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.
@@ -383,14 +392,14 @@
  * @retval non-zero failure
  */
 #define cxReallocateArray(allocator, mem, nmemb, size) \
-        cxReallocateArray(allocator, (void**) (mem), nmemb, size)
+        cxReallocateArray_(allocator, (void**) (mem), nmemb, size)
 
 /**
- * Allocate @p nelem elements of @p n bytes each, all initialized to zero.
+ * Allocate @p nmemb 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 nmemb the number of elements
+ * @param size the size of each element in bytes
  * @return a pointer to the allocated memory
  */
 cx_attr_nonnull_arg(1)
@@ -398,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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/array_list.h	Tue Feb 25 21:11:00 2025 +0100
@@ -47,6 +47,7 @@
  * 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;
 
 /**
@@ -218,6 +219,7 @@
 /**
  * A default stdlib-based array reallocator.
  */
+cx_attr_export
 extern CxArrayReallocator *cx_array_default_reallocator;
 
 /**
@@ -238,6 +240,7 @@
  * on the stack or shall not reallocated in place
  * @return an array reallocator
  */
+cx_attr_export
 CxArrayReallocator cx_array_reallocator(
         const struct cx_allocator_s *allocator,
         const void *stackmem
@@ -274,6 +277,7 @@
  * @see cx_array_reallocator()
  */
 cx_attr_nonnull_arg(1, 2, 3)
+cx_attr_export
 int cx_array_reserve(
         void **array,
         void *size,
@@ -317,6 +321,7 @@
  * @see cx_array_reallocator()
  */
 cx_attr_nonnull_arg(1, 2, 3, 6)
+cx_attr_export
 int cx_array_copy(
         void **target,
         void *size,
@@ -475,6 +480,7 @@
  * @retval non-zero failure
  */
 cx_attr_nonnull_arg(1, 2, 3, 5)
+cx_attr_export
 int cx_array_insert_sorted(
         void **target,
         size_t *size,
@@ -601,6 +607,7 @@
  * @see cx_array_binary_search()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cx_array_binary_search_inf(
         const void *arr,
         size_t size,
@@ -626,6 +633,7 @@
  * @see cx_array_binary_search_sup()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cx_array_binary_search(
         const void *arr,
         size_t size,
@@ -657,6 +665,7 @@
  * @see cx_array_binary_search()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cx_array_binary_search_sup(
         const void *arr,
         size_t size,
@@ -674,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,
@@ -684,9 +694,9 @@
 /**
  * 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)
@@ -700,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,
@@ -714,9 +725,9 @@
  * 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 (@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
--- a/ucx/cx/buffer.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/buffer.h	Tue Feb 25 21:11:00 2025 +0100
@@ -89,6 +89,17 @@
 #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 {
@@ -128,7 +139,7 @@
 };
 
 /**
- * Type alais for the flush configuration struct.
+ * Type alias for the flush configuration struct.
  *
  * @code
  * struct cx_buffer_flush_config_s {
@@ -162,7 +173,7 @@
      *
      * @see cxBufferEnableFlushing()
      */
-    CxBufferFlushConfig* flush;
+    CxBufferFlushConfig *flush;
     /** Current position of the buffer. */
     size_t pos;
     /** Current capacity (i.e. maximum size) of the buffer. */
@@ -216,6 +227,7 @@
  * @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,
@@ -239,6 +251,7 @@
  * @see cxBufferWrite()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferEnableFlushing(
     CxBuffer *buffer,
     CxBufferFlushConfig config
@@ -254,6 +267,7 @@
  * @see cxBufferInit()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxBufferDestroy(CxBuffer *buffer);
 
 /**
@@ -268,6 +282,7 @@
  * @param buffer the buffer to deallocate
  * @see cxBufferCreate()
  */
+cx_attr_export
 void cxBufferFree(CxBuffer *buffer);
 
 /**
@@ -297,6 +312,7 @@
 cx_attr_malloc
 cx_attr_dealloc(cxBufferFree, 1)
 cx_attr_nodiscard
+cx_attr_export
 CxBuffer *cxBufferCreate(
         void *space,
         size_t capacity,
@@ -341,6 +357,7 @@
  * @see cxBufferShiftRight()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferShift(
         CxBuffer *buffer,
         off_t shift
@@ -357,6 +374,7 @@
  * @see cxBufferShift()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferShiftRight(
         CxBuffer *buffer,
         size_t shift
@@ -373,6 +391,7 @@
  * @see cxBufferShift()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferShiftLeft(
         CxBuffer *buffer,
         size_t shift
@@ -400,6 +419,7 @@
  *
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferSeek(
         CxBuffer *buffer,
         off_t offset,
@@ -419,6 +439,7 @@
  * @see cxBufferReset()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxBufferClear(CxBuffer *buffer);
 
 /**
@@ -431,6 +452,7 @@
  * @see cxBufferClear()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxBufferReset(CxBuffer *buffer);
 
 /**
@@ -443,6 +465,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 bool cxBufferEof(const CxBuffer *buffer);
 
 
@@ -457,6 +480,7 @@
  * @retval non-zero on allocation failure
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferMinimumCapacity(
         CxBuffer *buffer,
         size_t capacity
@@ -473,14 +497,25 @@
  * 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.
- * If number of items that shall be written is larger than the buffer can hold,
- * the first items from @c ptr are directly relayed to the flush target, if
- * possible.
- * The number returned by this function is only the number of elements from
- * @c ptr that could be written to either the flush target or the buffer.
+ *
+ * If, 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.
+ *
+ * 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).
+ *
+ * @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.
  *
@@ -493,6 +528,7 @@
  * @see cxBufferRead()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cxBufferWrite(
         const void *ptr,
         size_t size,
@@ -520,6 +556,7 @@
  * @see cxBufferRead()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cxBufferAppend(
         const void *ptr,
         size_t size,
@@ -581,6 +618,7 @@
  * @see cxBufferEnableFlushing()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cxBufferFlush(CxBuffer *buffer);
 
 /**
@@ -599,6 +637,7 @@
  * @see cxBufferAppend()
  */
 cx_attr_nonnull
+cx_attr_export
 size_t cxBufferRead(
         void *ptr,
         size_t size,
@@ -626,6 +665,7 @@
  * @see cxBufferTerminate()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferPut(
         CxBuffer *buffer,
         int c
@@ -644,6 +684,7 @@
  * @return zero, if the terminator could be written, non-zero otherwise
  */
 cx_attr_nonnull
+cx_attr_export
 int cxBufferTerminate(CxBuffer *buffer);
 
 /**
@@ -657,6 +698,7 @@
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(2)
+cx_attr_export
 size_t cxBufferPutString(
         CxBuffer *buffer,
         const char *str
@@ -671,6 +713,7 @@
  * @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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/collection.h	Tue Feb 25 21:11:00 2025 +0100
@@ -92,6 +92,11 @@
      * 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;
 };
 
 /**
@@ -108,6 +113,45 @@
 #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 a pointer to a struct that contains #CX_COLLECTION_BASE
--- a/ucx/cx/common.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/common.h	Tue Feb 25 21:11:00 2025 +0100
@@ -120,22 +120,6 @@
 #endif
 
 // ---------------------------------------------------------------------------
-//       Missing Defines
-// ---------------------------------------------------------------------------
-
-#ifndef SSIZE_MAX // not defined in glibc since C23 and MSVC
-#if CX_WORDSIZE == 64
-/**
- * The maximum representable value in ssize_t.
- */
-#define SSIZE_MAX 0x7fffffffffffffffll
-#else
-#define SSIZE_MAX 0x7fffffffl
-#endif
-#endif
-
-
-// ---------------------------------------------------------------------------
 //       Attribute definitions
 // ---------------------------------------------------------------------------
 
@@ -282,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
 // ---------------------------------------------------------------------------
@@ -356,21 +359,9 @@
 #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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/compare.h	Tue Feb 25 21:11:00 2025 +0100
@@ -56,6 +56,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 typedef int (*cx_compare_func)(
     const void *left,
     const void *right
@@ -75,10 +76,11 @@
  */
 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
@@ -87,6 +89,7 @@
  * @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);
 
 /**
@@ -103,10 +106,11 @@
  */
 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
@@ -115,6 +119,7 @@
  * @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);
 
 /**
@@ -131,10 +136,11 @@
  */
 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
@@ -143,6 +149,7 @@
  * @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);
 
 /**
@@ -159,6 +166,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_int16(const void *i1, const void *i2);
 
 /**
@@ -171,6 +179,7 @@
  * @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);
 
 /**
@@ -187,6 +196,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_int32(const void *i1, const void *i2);
 
 /**
@@ -199,6 +209,7 @@
  * @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);
 
 /**
@@ -215,6 +226,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_int64(const void *i1, const void *i2);
 
 /**
@@ -227,6 +239,7 @@
  * @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);
 
 /**
@@ -243,10 +256,11 @@
  */
 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
@@ -255,6 +269,7 @@
  * @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);
 
 /**
@@ -271,10 +286,11 @@
  */
 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
@@ -283,6 +299,7 @@
  * @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);
 
 /**
@@ -299,10 +316,11 @@
  */
 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
@@ -311,6 +329,7 @@
  * @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);
 
 /**
@@ -327,6 +346,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uint16(const void *i1, const void *i2);
 
 /**
@@ -339,6 +359,7 @@
  * @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);
 
 /**
@@ -355,6 +376,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uint32(const void *i1, const void *i2);
 
 /**
@@ -367,6 +389,7 @@
  * @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);
 
 /**
@@ -383,6 +406,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uint64(const void *i1, const void *i2);
 
 /**
@@ -395,6 +419,7 @@
  * @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);
 
 /**
@@ -411,6 +436,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_float(const void *f1, const void *f2);
 
 /**
@@ -423,6 +449,7 @@
  * @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);
 
 /**
@@ -439,10 +466,11 @@
  */
 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
@@ -451,6 +479,7 @@
  * @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);
 
 /**
@@ -467,6 +496,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_intptr(const void *ptr1, const void *ptr2);
 
 /**
@@ -479,6 +509,7 @@
  * @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);
 
 /**
@@ -495,6 +526,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cx_cmp_uintptr(const void *ptr1, const void *ptr2);
 
 /**
@@ -507,10 +539,11 @@
  * @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
@@ -520,6 +553,7 @@
  */
 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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/hash_key.h	Tue Feb 25 21:11:00 2025 +0100
@@ -76,6 +76,7 @@
  * @see cx_hash_key()
  */
 cx_attr_nonnull
+cx_attr_export
 void cx_hash_murmur(CxHashKey *key);
 
 /**
@@ -88,6 +89,7 @@
  */
 cx_attr_nodiscard
 cx_attr_cstr_arg(1)
+cx_attr_export
 CxHashKey cx_hash_key_str(const char *str);
 
 /**
@@ -99,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
@@ -117,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
--- a/ucx/cx/hash_map.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/hash_map.h	Tue Feb 25 21:11:00 2025 +0100
@@ -69,8 +69,8 @@
  *
  * 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.
@@ -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,8 +95,8 @@
 /**
  * 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.
@@ -126,6 +127,7 @@
  * @retval non-zero if a memory allocation error occurred
  */
 cx_attr_nonnull
+cx_attr_export
 int cxMapRehash(CxMap *map);
 
 
--- a/ucx/cx/iterator.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/iterator.h	Tue Feb 25 21:11:00 2025 +0100
@@ -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.
      */
@@ -174,8 +153,6 @@
 /**
  * Checks if the iterator points to valid data.
  *
- * This is especially false for past-the-end iterators.
- *
  * @param iter the iterator
  * @retval true if the iterator points to valid data
  * @retval false if the iterator already moved past the end
@@ -215,7 +192,7 @@
  * This is useful for APIs that expect some iterator as an argument.
  *
  * @param iter the iterator
- * @return (@c CxIterator*) a pointer to the iterator
+ * @return (@c struct @c cx_iterator_base_s*) a pointer to the iterator
  */
 #define cxIteratorRef(iter) &((iter).base)
 
@@ -248,6 +225,7 @@
  * @see cxIteratorPtr()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxIterator(
         const void *array,
         size_t elem_size,
@@ -278,6 +256,7 @@
  * @return an iterator for the specified array
  */
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxMutIterator(
         void *array,
         size_t elem_size,
@@ -299,6 +278,7 @@
  * @see cxIterator()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxIteratorPtr(
         const void *array,
         size_t elem_count
@@ -319,6 +299,7 @@
  * @see cxIteratorPtr()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxMutIteratorPtr(
         void *array,
         size_t elem_count,
--- a/ucx/cx/json.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/json.h	Tue Feb 25 21:11:00 2025 +0100
@@ -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
 };
 
 /**
@@ -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;
     /**
@@ -461,6 +458,10 @@
      * 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,13 +485,13 @@
  * @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.
@@ -506,6 +508,7 @@
  * @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,
@@ -521,6 +524,7 @@
  * @see cxJsonDestroy()
  */
 cx_attr_nonnull_arg(1)
+cx_attr_export
 void cxJsonInit(CxJson *json, const CxAllocator *allocator);
 
 /**
@@ -530,6 +534,7 @@
  * @see cxJsonInit()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxJsonDestroy(CxJson *json);
 
 /**
@@ -567,6 +572,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonFilln(CxJson *json, const char *buf, size_t len);
 
 #ifdef __cplusplus
@@ -667,6 +673,7 @@
  * @see cxJsonArrAddValues()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateObj(const CxAllocator* allocator);
 
 /**
@@ -678,6 +685,7 @@
  * @see cxJsonArrAddValues()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateArr(const CxAllocator* allocator);
 
 /**
@@ -690,6 +698,7 @@
  * @see cxJsonArrAddNumbers()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateNumber(const CxAllocator* allocator, double num);
 
 /**
@@ -702,6 +711,7 @@
  * @see cxJsonArrAddIntegers()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateInteger(const CxAllocator* allocator, int64_t num);
 
 /**
@@ -717,6 +727,7 @@
 cx_attr_nodiscard
 cx_attr_nonnull_arg(2)
 cx_attr_cstr_arg(2)
+cx_attr_export
 CxJsonValue* cxJsonCreateString(const CxAllocator* allocator, const char *str);
 
 /**
@@ -730,6 +741,7 @@
  * @see cxJsonArrAddCxStrings()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateCxString(const CxAllocator* allocator, cxstring str);
 
 /**
@@ -742,6 +754,7 @@
  * @see cxJsonArrAddLiterals()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxJsonValue* cxJsonCreateLiteral(const CxAllocator* allocator, CxJsonLiteral lit);
 
 /**
@@ -755,6 +768,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddNumbers(CxJsonValue* arr, const double* num, size_t count);
 
 /**
@@ -768,6 +782,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddIntegers(CxJsonValue* arr, const int64_t* num, size_t count);
 
 /**
@@ -784,6 +799,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddStrings(CxJsonValue* arr, const char* const* str, size_t count);
 
 /**
@@ -800,6 +816,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddCxStrings(CxJsonValue* arr, const cxstring* str, size_t count);
 
 /**
@@ -813,6 +830,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddLiterals(CxJsonValue* arr, const CxJsonLiteral* lit, size_t count);
 
 /**
@@ -829,6 +847,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxJsonArrAddValues(CxJsonValue* arr, CxJsonValue* const* val, size_t count);
 
 /**
@@ -846,6 +865,7 @@
  * @retval non-zero allocation failure
  */
 cx_attr_nonnull
+cx_attr_export
 int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child);
 
 /**
@@ -858,6 +878,7 @@
  * @see cxJsonCreateObj()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name);
 
 /**
@@ -870,6 +891,7 @@
  * @see cxJsonCreateArr()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutArr(CxJsonValue* obj, cxstring name);
 
 /**
@@ -883,6 +905,7 @@
  * @see cxJsonCreateNumber()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutNumber(CxJsonValue* obj, cxstring name, double num);
 
 /**
@@ -896,6 +919,7 @@
  * @see cxJsonCreateInteger()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutInteger(CxJsonValue* obj, cxstring name, int64_t num);
 
 /**
@@ -912,6 +936,7 @@
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(3)
+cx_attr_export
 CxJsonValue* cxJsonObjPutString(CxJsonValue* obj, cxstring name, const char* str);
 
 /**
@@ -927,6 +952,7 @@
  * @see cxJsonCreateCxString()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutCxString(CxJsonValue* obj, cxstring name, cxstring str);
 
 /**
@@ -940,6 +966,7 @@
  * @see cxJsonCreateLiteral()
  */
 cx_attr_nonnull
+cx_attr_export
 CxJsonValue* cxJsonObjPutLiteral(CxJsonValue* obj, cxstring name, CxJsonLiteral lit);
 
 /**
@@ -953,6 +980,7 @@
  *
  * @param value the value
  */
+cx_attr_export
 void cxJsonValueFree(CxJsonValue *value);
 
 /**
@@ -979,6 +1007,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_w(2)
+cx_attr_export
 CxJsonStatus cxJsonNext(CxJson *json, CxJsonValue **value);
 
 /**
@@ -1251,6 +1280,7 @@
  */
 cx_attr_nonnull
 cx_attr_returns_nonnull
+cx_attr_export
 CxJsonValue *cxJsonArrGet(const CxJsonValue *value, size_t index);
 
 /**
@@ -1266,6 +1296,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxJsonArrIter(const CxJsonValue *value);
 
 /**
@@ -1282,6 +1313,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxJsonObjIter(const CxJsonValue *value);
 
 /**
@@ -1289,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));
 }
 
--- a/ucx/cx/linked_list.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/linked_list.h	Tue Feb 25 21:11:00 2025 +0100
@@ -44,17 +44,11 @@
 #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.
  *
- * 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)
@@ -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,
@@ -80,9 +75,9 @@
  * 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 (@c size_t) the size of each element in bytes
  * @return (@c CxList*) the created list
@@ -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,44 +113,26 @@
 );
 
 /**
- * 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 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
 );
 
 /**
@@ -170,6 +148,7 @@
  */
 cx_attr_nonnull
 cx_attr_returns_nonnull
+cx_attr_export
 void *cx_linked_list_first(
         const void *node,
         ptrdiff_t loc_prev
@@ -188,6 +167,7 @@
  */
 cx_attr_nonnull
 cx_attr_returns_nonnull
+cx_attr_export
 void *cx_linked_list_last(
         const void *node,
         ptrdiff_t loc_next
@@ -204,6 +184,7 @@
  * @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,
@@ -223,6 +204,7 @@
  * @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,
@@ -244,6 +226,7 @@
  * @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,
@@ -261,6 +244,7 @@
  * @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,
@@ -279,6 +263,7 @@
  * @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,
@@ -301,6 +286,7 @@
  * @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,
@@ -331,6 +317,7 @@
  * @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,
@@ -356,6 +343,7 @@
  * @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,
@@ -385,6 +373,7 @@
  * @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,
@@ -416,6 +405,7 @@
  * @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,
@@ -462,6 +452,8 @@
  * @param loc_next the location of the @c next pointer within the node struct
  * @return the size of the list or zero if @p node is @c NULL
  */
+cx_attr_nodiscard
+cx_attr_export
 size_t cx_linked_list_size(
         const void *node,
         ptrdiff_t loc_next
@@ -490,6 +482,7 @@
  * @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,
@@ -514,6 +507,7 @@
  * 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,
@@ -531,6 +525,7 @@
  * @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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/list.h	Tue Feb 25 21:11:00 2025 +0100
@@ -80,7 +80,6 @@
 
     /**
      * Member function for inserting a single element.
-     * Implementors SHOULD see to performant implementations for corner cases.
      */
     int (*insert_element)(
             struct cx_list_s *list,
@@ -90,7 +89,6 @@
 
     /**
      * Member function for inserting multiple elements.
-     * Implementors SHOULD see to performant implementations for corner cases.
      *
      * @see cx_list_default_insert_array()
      */
@@ -165,7 +163,7 @@
     /**
      * Member function for finding and optionally removing an element.
      */
-    ssize_t (*find_remove)(
+    size_t (*find_remove)(
             struct cx_list_s *list,
             const void *elem,
             bool remove
@@ -219,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,
@@ -243,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,
@@ -261,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);
 
 /**
@@ -277,53 +278,69 @@
  * 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
@@ -348,6 +365,7 @@
         CxList *list,
         const void *elem
 ) {
+    list->collection.sorted = false;
     return list->cl->insert_element(list, list->collection.size, elem);
 }
 
@@ -373,6 +391,7 @@
         const void *array,
         size_t n
 ) {
+    list->collection.sorted = false;
     return list->cl->insert_array(list, list->collection.size, array, n);
 }
 
@@ -395,12 +414,15 @@
         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
  * @retval zero success
@@ -411,6 +433,7 @@
         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;
 }
@@ -441,6 +464,7 @@
         const void *array,
         size_t n
 ) {
+    list->collection.sorted = false;
     return list->cl->insert_array(list, index, array, n);
 }
 
@@ -456,6 +480,8 @@
  * 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
@@ -467,6 +493,7 @@
         const void *array,
         size_t n
 ) {
+    list->collection.sorted = true; // guaranteed by definition
     return list->cl->insert_sorted(list, array, n);
 }
 
@@ -491,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);
 }
 
 /**
@@ -515,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);
 }
 
 /**
@@ -616,6 +647,7 @@
  */
 cx_attr_nonnull
 static inline void cxListClear(CxList *list) {
+    list->collection.sorted = true; // empty lists are always sorted
     list->cl->clear(list);
 }
 
@@ -638,6 +670,7 @@
         size_t i,
         size_t j
 ) {
+    list->collection.sorted = false;
     return list->cl->swap(list, i, j);
 }
 
@@ -709,6 +742,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxListMutIteratorAt(
         CxList *list,
         size_t index
@@ -728,6 +762,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxIterator cxListMutBackwardsIteratorAt(
         CxList *list,
         size_t index
@@ -805,12 +840,12 @@
  *
  * @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
 ) {
@@ -818,17 +853,32 @@
 }
 
 /**
+ * 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
 ) {
@@ -845,6 +895,7 @@
 cx_attr_nonnull
 static inline void cxListSort(CxList *list) {
     list->cl->sort(list);
+    list->collection.sorted = true;
 }
 
 /**
@@ -854,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);
 }
 
@@ -873,6 +926,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 int cxListCompare(
         const CxList *list,
         const CxList *other
@@ -885,6 +939,7 @@
  *
  * @param list the list which shall be freed
  */
+cx_attr_export
 void cxListFree(CxList *list);
 
 /**
@@ -895,6 +950,7 @@
  * 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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/map.h	Tue Feb 25 21:11:00 2025 +0100
@@ -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,6 +100,76 @@
 };
 
 /**
+ * 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 {
@@ -132,21 +219,7 @@
     /**
      * Creates an iterator for this map.
      */
-    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);
 };
 
 /**
@@ -157,59 +230,17 @@
  * 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_export
 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()
- */
-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;
-}
-
-/**
  * 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
  */
+cx_attr_export
 void cxMapFree(CxMap *map);
 
 
@@ -239,6 +270,10 @@
 /**
  * Creates a value iterator for a map.
  *
+ * 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.
  *
@@ -247,14 +282,15 @@
  */
 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
  * highly depends on the map implementation and may change arbitrarily when the contents change.
@@ -264,14 +300,15 @@
  */
 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
  * highly depends on the map implementation and may change arbitrarily when the contents change.
@@ -283,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);
 }
 
@@ -291,6 +328,10 @@
 /**
  * Creates a mutating iterator over the values of a map.
  *
+ * 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.
  *
@@ -299,12 +340,14 @@
  */
 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
  * highly depends on the map implementation and may change arbitrarily when the contents change.
@@ -314,12 +357,14 @@
  */
 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
  * highly depends on the map implementation and may change arbitrarily when the contents change.
@@ -331,7 +376,8 @@
  */
 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
@@ -538,6 +584,8 @@
  * Puts a key/value-pair into the map.
  *
  * 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
@@ -749,7 +797,7 @@
  * Removes a key/value-pair from the map by using the key.
  *
  * This function will copy the contents of the removed element
- * to the target buffer must be guaranteed to be large enough
+ * to 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.
  *
@@ -761,8 +809,7 @@
  * @param targetbuf (@c void*) the buffer where the element shall be copied to
  * @retval zero success
  * @retval non-zero the key was not found
- * 
- * @see cxMapStorePointers()
+ *
  * @see cxMapRemove()
  */
 #define cxMapRemoveAndGet(map, key, targetbuf) _Generic((key), \
--- a/ucx/cx/mempool.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/mempool.h	Tue Feb 25 21:11:00 2025 +0100
@@ -80,6 +80,7 @@
  *
  * @param pool the memory pool to free
  */
+cx_attr_export
 void cxMempoolFree(CxMempool *pool);
 
 /**
@@ -94,6 +95,7 @@
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxMempoolFree, 1)
+cx_attr_export
 CxMempool *cxMempoolCreate(size_t capacity, cx_destructor_func destr);
 
 /**
@@ -102,7 +104,7 @@
  * @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);
 
 /**
@@ -145,6 +149,7 @@
  * @retval non-zero failure
  */
 cx_attr_nonnull
+cx_attr_export
 int cxMempoolRegister(
         CxMempool *pool,
         void *memory,
--- a/ucx/cx/printf.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/printf.h	Tue Feb 25 21:11:00 2025 +0100
@@ -56,6 +56,7 @@
 /**
  * The maximum string length that fits into stack memory.
  */
+cx_attr_export
 extern const unsigned cx_printf_sbo_size;
 
 /**
@@ -71,6 +72,7 @@
 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,
@@ -91,6 +93,7 @@
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(3)
+cx_attr_export
 int cx_vfprintf(
         void *stream,
         cx_write_func wfc,
@@ -115,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,
@@ -153,6 +157,7 @@
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(2)
+cx_attr_export
 cxmutstr cx_vasprintf_a(
         const CxAllocator *allocator,
         const char *fmt,
@@ -185,7 +190,7 @@
  * @see cxBufferWrite()
  */
 #define cx_bprintf(buffer, fmt, ...) cx_fprintf((void*)buffer, \
-    (cx_write_func) cxBufferWrite, fmt, __VA_ARGS__)
+    cxBufferWriteFunc, fmt, __VA_ARGS__)
 
 
 /**
@@ -222,6 +227,7 @@
 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,
@@ -266,6 +272,7 @@
 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,
@@ -324,6 +331,7 @@
 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,
@@ -378,6 +386,7 @@
  */
 cx_attr_nonnull
 cx_attr_cstr_arg(5)
+cx_attr_export
 int cx_vsprintf_sa(
         CxAllocator *alloc,
         char *buf,
--- a/ucx/cx/properties.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/properties.h	Tue Feb 25 21:11:00 2025 +0100
@@ -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;
 
 /**
@@ -327,6 +331,7 @@
  * @see cxPropertiesInitDefault()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
 
 /**
@@ -341,6 +346,7 @@
  * @param prop the properties interface
  */
 cx_attr_nonnull
+cx_attr_export
 void cxPropertiesDestroy(CxProperties *prop);
 
 /**
@@ -390,6 +396,7 @@
  */
 cx_attr_nonnull
 cx_attr_access_r(2, 3)
+cx_attr_export
 int cxPropertiesFilln(
         CxProperties *prop,
         const char *buf,
@@ -495,6 +502,7 @@
  * @param capacity the capacity of the stack memory
  */
 cx_attr_nonnull
+cx_attr_export
 void cxPropertiesUseStack(
         CxProperties *prop,
         char *buf,
@@ -533,6 +541,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxPropertiesStatus cxPropertiesNext(
         CxProperties *prop,
         cxstring *key,
@@ -553,6 +562,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 CxPropertiesSink cxPropertiesMapSink(CxMap *map);
 
 /**
@@ -563,6 +573,7 @@
  * @see cxPropertiesLoad()
  */
 cx_attr_nodiscard
+cx_attr_export
 CxPropertiesSource cxPropertiesStringSource(cxstring str);
 
 /**
@@ -576,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);
 
 /**
@@ -591,6 +603,7 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_cstr_arg(1)
+cx_attr_export
 CxPropertiesSource cxPropertiesCstrSource(const char *str);
 
 /**
@@ -605,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);
 
 
@@ -616,6 +630,11 @@
  * 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
@@ -626,11 +645,13 @@
  * @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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/streams.h	Tue Feb 25 21:11:00 2025 +0100
@@ -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,
@@ -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,
--- a/ucx/cx/string.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/string.h	Tue Feb 25 21:11:00 2025 +0100
@@ -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;
 
 /**
@@ -51,7 +52,6 @@
     /**
      * A pointer to the string.
      * @note The string is not necessarily @c NULL terminated.
-     * Always use the length.
      */
     char *ptr;
     /** The length of the string */
@@ -70,7 +70,6 @@
     /**
      * A pointer to the immutable string.
      * @note The string is not necessarily @c NULL terminated.
-     * Always use the length.
      */
     const char *ptr;
     /** The length of the string */
@@ -175,6 +174,7 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_cstr_arg(1)
+cx_attr_export
 cxmutstr cx_mutstr(char *cstring);
 
 /**
@@ -195,6 +195,7 @@
  */
 cx_attr_nodiscard
 cx_attr_access_rw(1, 2)
+cx_attr_export
 cxmutstr cx_mutstrn(
         char *cstring,
         size_t length
@@ -218,6 +219,7 @@
 cx_attr_nonnull
 cx_attr_nodiscard
 cx_attr_cstr_arg(1)
+cx_attr_export
 cxstring cx_str(const char *cstring);
 
 
@@ -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
@@ -298,7 +301,8 @@
 /**
  * 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
  * you ever have a <code>const char*</code> you are really supposed to free.
@@ -306,12 +310,14 @@
  *
  * @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
  * you ever have a <code>const char*</code> you are really supposed to free.
@@ -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
@@ -339,6 +346,7 @@
  * @return the accumulated length of all strings
  */
 cx_attr_nodiscard
+cx_attr_export
 size_t cx_strlen(
         size_t count,
         ...
@@ -368,6 +376,7 @@
  */
 cx_attr_nodiscard
 cx_attr_nonnull
+cx_attr_export
 cxmutstr cx_strcat_ma(
         const CxAllocator *alloc,
         cxmutstr str,
@@ -456,6 +465,7 @@
  * @see cx_strsubsl_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strsubs(
         cxstring string,
         size_t start
@@ -481,6 +491,7 @@
  * @see cx_strsubsl_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strsubsl(
         cxstring string,
         size_t start,
@@ -503,6 +514,7 @@
  * @see cx_strsubsl()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strsubs_m(
         cxmutstr string,
         size_t start
@@ -528,6 +540,7 @@
  * @see cx_strsubsl()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strsubsl_m(
         cxmutstr string,
         size_t start,
@@ -547,6 +560,7 @@
  * @see cx_strchr_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strchr(
         cxstring string,
         int chr
@@ -565,6 +579,7 @@
  * @see cx_strchr()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strchr_m(
         cxmutstr string,
         int chr
@@ -583,6 +598,7 @@
  * @see cx_strrchr_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strrchr(
         cxstring string,
         int chr
@@ -601,6 +617,7 @@
  * @see cx_strrchr()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strrchr_m(
         cxmutstr string,
         int chr
@@ -623,6 +640,7 @@
  * @see cx_strstr_m()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strstr(
         cxstring haystack,
         cxstring needle
@@ -645,6 +663,7 @@
  * @see cx_strstr()
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strstr_m(
         cxmutstr haystack,
         cxstring needle
@@ -659,12 +678,13 @@
  * @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,
@@ -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,
@@ -712,12 +733,13 @@
  * @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,
@@ -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,
@@ -764,6 +787,7 @@
  * than @p s2, zero if both strings equal
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_strcmp(
         cxstring s1,
         cxstring s2
@@ -778,6 +802,7 @@
  * than @p s2, zero if both strings equal ignoring case
  */
 cx_attr_nodiscard
+cx_attr_export
 int cx_strcasecmp(
         cxstring s1,
         cxstring s2
@@ -795,6 +820,7 @@
  */
 cx_attr_nodiscard
 cx_attr_nonnull
+cx_attr_export
 int cx_strcmp_p(
         const void *s1,
         const void *s2
@@ -812,6 +838,7 @@
  */
 cx_attr_nodiscard
 cx_attr_nonnull
+cx_attr_export
 int cx_strcasecmp_p(
         const void *s1,
         const void *s2
@@ -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,45 +868,33 @@
 /**
  * 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 (@c CxAllocator*) the allocator to use
+ * @param string the string to duplicate
+ * @return (@c cxmutstr) a duplicate of the string
+ * @see cx_strdup()
+ * @see cx_strfree_a()
+ */
+#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().
  *
  * @note The returned string is guaranteed to be zero-terminated.
  *
- * @param string (@c cxstring) the string to duplicate
+ * @param string the string to duplicate
  * @return (@c cxmutstr) a duplicate of the string
  * @see cx_strdup_a()
- */
-#define cx_strdup(string) cx_strdup_a(cxDefaultAllocator, string)
-
-
-/**
- * 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 (@c CxAllocator*) the allocator to use
- * @param string (@c cxmutstr) the string to duplicate
- * @return (@c cxmutstr) a duplicate of the string
- * @see cx_strdup_m()
+ * @see cx_strfree()
  */
-#define cx_strdup_ma(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().
- *
- * @note The returned string is guaranteed to be zero-terminated.
- *
- * @param string (@c cxmutstr) the string to duplicate
- * @return (@c cxmutstr) a duplicate of the string
- * @see cx_strdup_ma()
- */
-#define cx_strdup_m(string) cx_strdup_a(cxDefaultAllocator, cx_strcast(string))
+#define cx_strdup(string) cx_strdup_a_(cxDefaultAllocator, string)
 
 /**
  * Omits leading and trailing spaces.
@@ -890,6 +906,7 @@
  * @return the trimmed string
  */
 cx_attr_nodiscard
+cx_attr_export
 cxstring cx_strtrim(cxstring string);
 
 /**
@@ -902,6 +919,7 @@
  * @return the trimmed string
  */
 cx_attr_nodiscard
+cx_attr_export
 cxmutstr cx_strtrim_m(cxmutstr string);
 
 /**
@@ -913,6 +931,7 @@
  * @c false otherwise
  */
 cx_attr_nodiscard
+cx_attr_export
 bool cx_strprefix(
         cxstring string,
         cxstring prefix
@@ -927,6 +946,7 @@
  * @c false otherwise
  */
 cx_attr_nodiscard
+cx_attr_export
 bool cx_strsuffix(
         cxstring string,
         cxstring suffix
@@ -941,6 +961,7 @@
  * @c false otherwise
  */
 cx_attr_nodiscard
+cx_attr_export
 bool cx_strcaseprefix(
         cxstring string,
         cxstring prefix
@@ -955,35 +976,15 @@
  * @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.
- *
- * @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
@@ -994,25 +995,25 @@
  *
  * @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.
  *
  * The returned string will be allocated by @c malloc() and is guaranteed
@@ -1022,18 +1023,16 @@
  * the returned string will be empty.
  *
  * @param str (@c cxstring) the string where replacements should be applied
- * @param pattern (@c cxstring) the pattern to search for
+ * @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.
- *
- * The pattern is taken literally and is no regular expression.
+ * Replaces a string with another string.
  *
  * The returned string will be allocated by @p allocator and is guaranteed
  * to be zero-terminated.
@@ -1043,18 +1042,15 @@
  *
  * @param allocator (@c CxAllocator*) the allocator to use
  * @param str (@c cxstring) the string where replacements should be applied
- * @param pattern (@c cxstring) the pattern to search for
+ * @param 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.
- *
- * The pattern is taken literally and is no regular expression.
- * Replaces at most @p replmax occurrences.
+ * Replaces a string with another string.
  *
  * The returned string will be allocated by @c malloc() and is guaranteed
  * to be zero-terminated.
@@ -1063,12 +1059,12 @@
  * the returned string will be empty.
  *
  * @param str (@c cxstring) the string where replacements should be applied
- * @param pattern (@c cxstring) the pattern to search for
+ * @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.
@@ -1257,8 +1441,8 @@
  * @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);
+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.
@@ -1267,10 +1451,6 @@
  * 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
@@ -1278,8 +1458,8 @@
  * @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);
+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.
@@ -1288,10 +1468,6 @@
  * 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
@@ -1299,78 +1475,265 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-cx_attr_access_w(2) cx_attr_nonnull_arg(2)
-int cx_strtod_lc(cxstring str, double *output, char decsep, const char *groupsep);
+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);
 
-#ifndef CX_STR_IMPLEMENTATION
 /**
- * @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 (@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)
+#define cx_strtos_lc(str, output, base, groupsep) cx_strtos_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtoi_lc(str, output, base, groupsep) cx_strtoi_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtol_lc(str, output, base, groupsep) cx_strtol_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtoll_lc(str, output, base, groupsep) cx_strtoll_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtoi8_lc(str, output, base, groupsep) cx_strtoi8_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtoi16_lc(str, output, base, groupsep) cx_strtoi16_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtoi32_lc(str, output, base, groupsep) cx_strtoi32_lc_(cx_strcast(str), output, base, 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 (@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)
+#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()
+ * 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)
+#define cx_strtous_lc(str, output, base, groupsep) cx_strtous_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtou_lc(str, output, base, groupsep) cx_strtou_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtoul_lc(str, output, base, groupsep) cx_strtoul_lc_(cx_strcast(str), output, base, 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 (@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)
+#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()
+ * 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)
+#define cx_strtou8_lc(str, output, base, groupsep) cx_strtou8_lc_(cx_strcast(str), output, base, 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 (@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)
+#define cx_strtou16_lc(str, output, base, groupsep) cx_strtou16_lc_(cx_strcast(str), output, base, 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 (@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)
+#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.
  *
@@ -1381,80 +1744,156 @@
  * @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
+ * @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_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_strtouz_lc(str, output, base, groupsep) cx_strtouz_lc(cx_strcast(str), output, base, groupsep)
+#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, ",")
 
 /**
- * @copydoc cx_strtouz()
- */
-#define cx_strtos(str, output, base) cx_strtos_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
- */
-#define cx_strtoi(str, output, base) cx_strtoi_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
- */
-#define cx_strtol(str, output, base) cx_strtol_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
- */
-#define cx_strtoll(str, output, base) cx_strtoll_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
- */
-#define cx_strtoi8(str, output, base) cx_strtoi8_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
- */
-#define cx_strtoi16(str, output, base) cx_strtoi16_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
- */
-#define cx_strtoi32(str, output, base) cx_strtoi32_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
- */
-#define cx_strtoi64(str, output, base) cx_strtoi64_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
+ * 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(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
- */
-#define cx_strtous(str, output, base) cx_strtous_lc(str, output, base, ",")
+#define cx_strtoi16(str, output, base) cx_strtoi16_lc_(cx_strcast(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()
+ * 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(str, output, base, ",")
+#define cx_strtoi32(str, output, base) cx_strtoi32_lc_(cx_strcast(str), output, base, ",")
+
 /**
- * @copydoc cx_strtouz()
- */
-#define cx_strtou8(str, output, base) cx_strtou8_lc(str, output, base, ",")
-/**
- * @copydoc cx_strtouz()
+ * 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(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, ",")
+#define cx_strtoi64(str, output, base) cx_strtoi64_lc_(cx_strcast(str), output, base, ",")
+
 /**
  * Converts a string to a number.
  *
@@ -1463,7 +1902,61 @@
  * 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()).
+ * 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
@@ -1471,7 +1964,97 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-#define cx_strtouz(str, output, base) cx_strtouz_lc(str, output, base, ",")
+#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.
@@ -1480,10 +2063,6 @@
  * 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
@@ -1491,17 +2070,14 @@
  * @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)
+#define cx_strtof_lc(str, output, decsep, groupsep) cx_strtof_lc_(cx_strcast(str), output, decsep, groupsep)
+
 /**
  * Converts a string to a double precision floating point number.
  *
  * The function returns non-zero when conversion is not possible.
  * In that case the function sets errno to EINVAL when the reason is an invalid character.
  *
- * The decimal separator is assumed to be a dot character.
- * The comma character is treated as group separator and ignored during parsing.
- * If you want to choose a different format, use cx_strtof_lc().
- *
  * @param str the string to convert
  * @param output a pointer to the double variable where the result shall be stored
  * @param decsep the decimal separator
@@ -1509,7 +2085,7 @@
  * @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)
+#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.
@@ -1527,7 +2103,8 @@
  * @retval zero success
  * @retval non-zero conversion was not possible
  */
-#define cx_strtof(str, output) cx_strtof_lc(str, output, '.', ",")
+#define cx_strtof(str, output) cx_strtof_lc_(cx_strcast(str), output, '.', ",")
+
 /**
  * Converts a string to a double precision floating point number.
  *
@@ -1543,9 +2120,7 @@
  * @retval zero success
  * @retval non-zero 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/tree.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/cx/tree.h	Tue Feb 25 21:11:00 2025 +0100
@@ -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,
@@ -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,
@@ -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,
@@ -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;
 
 /**
@@ -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,
@@ -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,
@@ -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,
@@ -894,6 +904,7 @@
  * @see cxTreeFree()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxTreeDestroySubtree(CxTree *tree, void *node);
 
 
@@ -932,6 +943,7 @@
  *
  * @param tree the tree to free
  */
+cx_attr_export
 void cxTreeFree(CxTree *tree);
 
 /**
@@ -962,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,
@@ -1022,6 +1035,7 @@
 cx_attr_nodiscard
 cx_attr_malloc
 cx_attr_dealloc(cxTreeFree, 1)
+cx_attr_export
 CxTree *cxTreeCreateWrapped(
         const CxAllocator *allocator,
         void *root,
@@ -1067,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);
@@ -1158,6 +1172,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
 
 /**
@@ -1169,6 +1184,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
 
 /**
@@ -1179,6 +1195,7 @@
  */
 cx_attr_nonnull
 cx_attr_nodiscard
+cx_attr_export
 size_t cxTreeDepth(CxTree *tree);
 
 /**
@@ -1267,6 +1284,7 @@
  * @see cxTreeAddChildNode()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxTreeSetParent(
         CxTree *tree,
         void *parent,
@@ -1289,6 +1307,7 @@
  * @see cxTreeSetParent()
  */
 cx_attr_nonnull
+cx_attr_export
 void cxTreeAddChildNode(
         CxTree *tree,
         void *parent,
@@ -1313,6 +1332,7 @@
  * @see cxTreeInsert()
  */
 cx_attr_nonnull
+cx_attr_export
 int cxTreeAddChild(
         CxTree *tree,
         void *parent,
@@ -1353,6 +1373,7 @@
  * @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,
@@ -1371,6 +1392,7 @@
  * @param node the node to remove
  */
 cx_attr_nonnull
+cx_attr_export
 void cxTreeRemoveSubtree(CxTree *tree, void *node);
 
 /**
@@ -1392,6 +1414,7 @@
  * @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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/hash_map.c	Tue Feb 25 21:11:00 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 {
@@ -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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/json.c	Tue Feb 25 21:11:00 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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/linked_list.c	Tue Feb 25 21:11:00 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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/list.c	Tue Feb 25 21:11:00 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
--- a/ucx/map.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/map.c	Tue Feb 25 21:11:00 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,38 @@
 };
 
 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;
 }
--- a/ucx/properties.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/properties.c	Tue Feb 25 21:11:00 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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/string.c	Tue Feb 25 21:11:00 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	Mon Jan 06 22:22:55 2025 +0100
+++ b/ucx/tree.c	Tue Feb 25 21:11:00 2025 +0100
@@ -749,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;
--- a/ui/cocoa/EventData.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/cocoa/EventData.h	Tue Feb 25 21:11:00 2025 +0100
@@ -29,15 +29,24 @@
 #import "../ui/toolkit.h"
 #import "../common/context.h"
 
+typedef void(*get_eventdata_func)(id sender, UiVar *var, void **eventdata, int *value);
+
 @interface EventData : NSObject
-@property UiObject    *obj;
-@property ui_callback callback;
-@property void        *userdata;
-@property void        *data;
-@property int         value;
+@property UiObject           *obj;
+@property UiVar              *var;
+@property int                vartype;
+@property ui_callback        callback;
+@property void               *userdata;
+@property void               *data;
+@property int                value;
+@property get_eventdata_func get_eventdata;
 
 - (EventData*)init:(ui_callback)cb userdata:(void*)userdata;
 
 - (void)handleEvent:(id)sender;
 
+- (void)handleEventWithEventData:(id)sender;
+
+- (SEL)addDynamicMethod:(unsigned long long)method_id;
+
 @end
--- a/ui/cocoa/EventData.m	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/cocoa/EventData.m	Tue Feb 25 21:11:00 2025 +0100
@@ -28,6 +28,9 @@
 
 #import "EventData.h"
 
+#import <objc/runtime.h>
+
+
 @implementation EventData
 
 - (EventData*)init:(ui_callback)cb userdata:(void*)userdata {
@@ -37,7 +40,7 @@
 }
 
 - (void)handleEvent:(id)sender {
-    if(self.callback) {
+    if(_callback) {
         UiEvent event;
         event.obj = self.obj;
         event.window = event.obj->window;
@@ -48,5 +51,19 @@
     }
 }
 
+- (void)handleEventWithEventData:(id)sender {
+    UiEvent event;
+    event.obj = self.obj;
+    event.window = event.obj->window;
+    event.document = event.obj->ctx->document;
+    event.eventdata = NULL;
+    event.intval = 0;
+    if(_get_eventdata) {
+        _get_eventdata(sender, _var, &event.eventdata, &event.intval);
+    }
+    if(self.callback) {
+        self.callback(&event, self.userdata);
+    }
+}
 
 @end
--- a/ui/cocoa/GridLayout.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/cocoa/GridLayout.h	Tue Feb 25 21:11:00 2025 +0100
@@ -39,6 +39,8 @@
     int y;
     int colspan;
     int rowspan;
+    int preferred_width;
+    int preferred_height;
     BOOL hexpand;
     BOOL vexpand;
     BOOL hfill;
@@ -49,7 +51,7 @@
     int size;
     int pos;
     int preferred_size;
-    BOOL extend;
+    BOOL expand;
 } GridDef;
 
 @interface GridLayout : NSView<Container>
--- a/ui/cocoa/GridLayout.m	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/cocoa/GridLayout.m	Tue Feb 25 21:11:00 2025 +0100
@@ -62,54 +62,129 @@
     int ncols = _cols+1;
     int nrows = _rows+1;
     
-    GridDef *coldef = calloc(ncols, sizeof(GridDef));
-    GridDef *rowdef = calloc(nrows, sizeof(GridDef));
+    GridDef *cols = calloc(ncols, sizeof(GridDef));
+    GridDef *rows = calloc(nrows, sizeof(GridDef));
     
     NSRect viewFrame = self.frame;
     
     int colspacing = _columnspacing;
     int rowspacing = _rowspacing;
     
-    CxIterator i = cxListIterator(_children);
-    cx_foreach(GridElm *, elm, i) {
-        NSSize size = elm->view.intrinsicContentSize;
-        NSEdgeInsets alignment = elm->view.alignmentRectInsets;
-        if(size.width != NSViewNoIntrinsicMetric) {
-            CGFloat width = size.width + alignment.left + alignment.right;
-            if(width > coldef[elm->x].preferred_size) {
-                coldef[elm->x].preferred_size = width;
+    int span_max = 1;
+    for(int r=0;r<2;r++) {
+        CxIterator i = cxListIterator(_children);
+        cx_foreach(GridElm *, elm, i) {
+            int x = elm->x;
+            int y = elm->y;
+            GridDef *col = &cols[x];
+            GridDef *row = &rows[y];
+            
+            NSSize size = elm->view.intrinsicContentSize;
+            NSEdgeInsets alignment = elm->view.alignmentRectInsets;
+            if(size.width != NSViewNoIntrinsicMetric) {
+                CGFloat width = size.width + alignment.left + alignment.right;
+                if(width > cols[elm->x].preferred_size && elm->colspan <= 1 && span_max == 1) {
+                    cols[elm->x].preferred_size = width;
+                }
+                elm->preferred_width = width;
+            }
+            if(size.height != NSViewNoIntrinsicMetric) {
+                CGFloat height = size.height + alignment.top + alignment.right;
+                //CGFloat height = size.height;
+                if(height > rows[elm->y].preferred_size && elm->rowspan <= 1 && span_max == 1) {
+                    rows[elm->y].preferred_size = height;
+                }
+                elm->preferred_height = height;
+            }
+            
+            if(elm->rowspan > span_max || elm->colspan > span_max) {
+                continue;
+            }
+            
+            int end_col = x+elm->colspan;
+            if(end_col > ncols) {
+                end_col = ncols;
+            }
+            int end_row = y+elm->rowspan;
+            if(end_row > nrows) {
+                end_row = nrows;
+            }
+            
+            // are all columns in the span > preferred_width?
+            if(elm->colspan > 1) {
+                int span_width = 0;
+                GridDef *last_col = col;
+                for(int c=x;c<end_col;c++) {
+                    span_width += cols[c].size;
+                    last_col = &cols[c];
+                }
+                if(span_width < elm->preferred_width) {
+                    last_col->size += elm->preferred_width - span_width;
+                }
+            }
+            // are all rows in the span > preferred_height?
+            if(elm->rowspan > 1) {
+                int span_height = 0;
+                GridDef *last_row = row;
+                for(int c=x;c<end_row;c++) {
+                    span_height += rows[c].size;
+                    last_row = &rows[c];
+                }
+                if(span_height < elm->preferred_height) {
+                    last_row->size += elm->preferred_height - span_height;
+                }
+            }
+            
+            if(elm->hexpand) {
+                if(elm->colspan > 1) {
+                    // check if any column in the span is expanding
+                    // if not, make the last column expanding
+                    GridDef *last_col = col;
+                    for(int c=x;c<end_col;c++) {
+                        last_col = &cols[c];
+                        if(last_col->expand) {
+                            break;
+                        }
+                    }
+                    last_col->expand = TRUE;
+                } else {
+                    col->expand = TRUE;
+                }
+            }
+            if(elm->vexpand) {
+                if(elm->rowspan > 1) {
+                    // same as colspan
+                    GridDef *last_row = row;
+                    for(int c=x;c<nrows;c++) {
+                        last_row = &rows[c];
+                        if(last_row->expand) {
+                            break;
+                        }
+                    }
+                    last_row->expand = TRUE;
+                } else {
+                    row->expand = TRUE;
+                }
             }
         }
-        if(size.height != NSViewNoIntrinsicMetric) {
-            CGFloat height = size.height + alignment.top + alignment.right;
-            //CGFloat height = size.height;
-            if(height > rowdef[elm->y].preferred_size) {
-                rowdef[elm->y].preferred_size = height;
-            }
-        }
-        
-        if(elm->hexpand) {
-            coldef[elm->x].extend = TRUE;
-        }
-        if(elm->vexpand) {
-            rowdef[elm->y].extend = TRUE;
-        }
+        span_max = 50000; // not sure if this is unreasonable low or high
     }
     
+    
     int col_ext = 0;
     int row_ext = 0;
     
     int preferred_width = 0;
     int preferred_height = 0;
     for(int j=0;j<ncols;j++) {
-        preferred_width += coldef[j].preferred_size + colspacing;
-        if(coldef[j].extend) {
+        preferred_width += cols[j].preferred_size + colspacing;
+        if(cols[j].expand) {
             col_ext++;
         }
     }
     for(int j=0;j<nrows;j++) {
-        preferred_height += rowdef[j].preferred_size + rowspacing;
-        if(rowdef[j].extend) {
+        preferred_height += rows[j].preferred_size + rowspacing;
+        if(rows[j].expand) {
             row_ext++;
         }
     }
@@ -124,16 +199,16 @@
     int vext = vremaining/row_ext;
     
     for(int j=0;j<ncols;j++) {
-        GridDef *col = &coldef[j];
-        if(col->extend) {
+        GridDef *col = &cols[j];
+        if(col->expand) {
             col->size = col->preferred_size + hext;
         } else {
             col->size = col->preferred_size;
         }
     }
     for(int j=0;j<nrows;j++) {
-        GridDef *row = &rowdef[j];
-        if(row->extend) {
+        GridDef *row = &rows[j];
+        if(row->expand) {
             row->size = row->preferred_size + vext;
         } else {
             row->size = row->preferred_size;
@@ -142,32 +217,63 @@
     
     int pos = 0;
     for(int j=0;j<ncols;j++) {
-        coldef[j].pos = pos;
-        pos += coldef[j].size + colspacing;
+        cols[j].pos = pos;
+        pos += cols[j].size + colspacing;
     }
     pos = 0;
     for(int j=0;j<nrows;j++) {
-        rowdef[j].pos = pos;
-        pos += rowdef[j].size + rowspacing;
+        rows[j].pos = pos;
+        pos += rows[j].size + rowspacing;
     }
     
-    i = cxListIterator(_children);
+    CxIterator i = cxListIterator(_children);
     cx_foreach(GridElm *, elm, i) {
         //NSSize size = elm->view.intrinsicContentSize;
-        GridDef *col = &coldef[elm->x];
-        GridDef *row = &rowdef[elm->y];
+        GridDef *col = &cols[elm->x];
+        GridDef *row = &rows[elm->y];
         
         NSEdgeInsets alignment = elm->view.alignmentRectInsets;
         NSRect frame;
-        frame.size.width = col->size;
-        frame.size.height = row->size;
+        if(elm->hfill) {
+            if(elm->colspan > 1) {
+                int cwidth = 0;
+                int end_col = elm->x + elm->colspan;
+                if(end_col > ncols) {
+                    end_col = ncols;
+                }
+                for(int c=elm->x;c<end_col;c++) {
+                    cwidth += cols[c].size;
+                }
+                frame.size.width = cwidth;
+            } else {
+                frame.size.width = col->size;
+            }
+        } else {
+            frame.size.width = elm->preferred_width;
+        }
+        if(elm->vfill) {
+            if(elm->rowspan > 1) {
+                int rheight = 0;
+                int end_row = elm->y + elm->rowspan;
+                if(end_row > nrows) {
+                    end_row = nrows;
+                }
+                for(int r=elm->y;r<end_row;r++) {
+                    rheight += rows[r].size;
+                }
+                frame.size.height = rheight;
+            }
+            frame.size.height = row->size;
+        } else {
+            frame.size.height = elm->preferred_height;
+        }
         frame.origin.x = col->pos - (alignment.left+alignment.right)/2;
         frame.origin.y = viewFrame.size.height - row->pos - frame.size.height + ((alignment.top+alignment.right)/2);
         elm->view.frame = frame;
     }
     
-    free(coldef);
-    free(rowdef);
+    free(cols);
+    free(rows);
 }
  
 
--- a/ui/cocoa/MainWindow.m	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/cocoa/MainWindow.m	Tue Feb 25 21:11:00 2025 +0100
@@ -46,8 +46,8 @@
                                defer:false];
     
     // create a vertical stackview as default container
-    //BoxContainer *vbox = [[BoxContainer alloc] init:NSUserInterfaceLayoutOrientationVertical spacing:0];
-    GridLayout *vbox = [[GridLayout alloc] init];
+    BoxContainer *vbox = [[BoxContainer alloc] init:NSUserInterfaceLayoutOrientationVertical spacing:0];
+    //GridLayout *vbox = [[GridLayout alloc] init];
     vbox.translatesAutoresizingMaskIntoConstraints = false;
     [self.contentView addSubview:vbox];
     [NSLayoutConstraint activateConstraints:@[
--- a/ui/cocoa/button.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/cocoa/button.h	Tue Feb 25 21:11:00 2025 +0100
@@ -29,3 +29,12 @@
 #import "toolkit.h"
 
 #import "../ui/button.h"
+
+int64_t ui_togglebutton_get(UiInteger *i);
+void ui_togglebutton_set(UiInteger *i, int64_t value);
+
+int64_t ui_switch_get(UiInteger *i);
+void ui_switch_set(UiInteger *i, int64_t value);
+
+int64_t ui_radiobuttons_get(UiInteger *i);
+void ui_radiobuttons_set(UiInteger *i, int64_t value);
--- a/ui/cocoa/button.m	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/cocoa/button.m	Tue Feb 25 21:11:00 2025 +0100
@@ -51,3 +51,178 @@
     
     return (__bridge void*)button;
 }
+
+
+static void togglebutton_eventdata(id button, UiVar *var, void **eventdata, int *value) {
+    NSButton *btn = (NSButton*)button;
+    NSControlStateValue state = btn.state;
+    *value = (int)state;
+}
+
+UIWIDGET togglebutton_create(UiObject* obj, UiToggleArgs args, enum NSButtonType type) {
+    NSButton *button = [[NSButton alloc] init];
+    [button setButtonType:type];
+    //[button setAllowsMixedState:YES];
+    
+    if(args.label) {
+        NSString *label = [[NSString alloc] initWithUTF8String:args.label];
+        button.title = label;
+    }
+    
+    UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args.value, args.varname, UI_VAR_INTEGER);
+    if(var) {
+        UiInteger *i = var->value;
+        i->obj = (__bridge void*)button;
+        i->get = ui_togglebutton_get;
+        i->set = ui_togglebutton_set;
+    }
+    
+    if(args.onchange) {
+        EventData *event = [[EventData alloc] init:args.onchange userdata:args.onchangedata];
+        event.get_eventdata = togglebutton_eventdata;
+        event.obj = obj;
+        event.var = var;
+        event.vartype = UI_VAR_INTEGER;
+        button.target = event;
+        button.action = @selector(handleEventWithEventData:);
+        objc_setAssociatedObject(button, "eventdata", event, OBJC_ASSOCIATION_RETAIN);
+    }
+    
+    UiLayout layout = UI_INIT_LAYOUT(args);
+    ui_container_add(obj, button, &layout, FALSE);
+    
+    return (__bridge void*)button;
+}
+
+int64_t ui_togglebutton_get(UiInteger *i) {
+    NSButton *button = (__bridge NSButton*)i->obj;
+    NSControlStateValue state = button.state;
+    i->value = (int64_t)state;
+    return (int64_t)state;
+}
+
+void ui_togglebutton_set(UiInteger *i, int64_t value) {
+    NSButton *button = (__bridge NSButton*)i->obj;
+    NSControlStateValue state;
+    switch(value) {
+        case 0: state = NSControlStateValueOff; break;
+        case 1: state = NSControlStateValueOff; break;
+        default: state = NSControlStateValueMixed;
+    }
+    i->value = (int64_t)state;
+    button.state = state;
+}
+
+UIWIDGET ui_togglebutton_create(UiObject* obj, UiToggleArgs args) {
+    return togglebutton_create(obj, args, NSButtonTypePushOnPushOff);
+}
+
+UIWIDGET ui_checkbox_create(UiObject* obj, UiToggleArgs args) {
+    return togglebutton_create(obj, args, NSButtonTypeSwitch);
+}
+
+static void switch_eventdata(id button, UiVar *var, void **eventdata, int *value) {
+    NSSwitch *btn = (NSSwitch*)button;
+    NSControlStateValue state = btn.state;
+    *value = (int)state;
+}
+
+UIWIDGET ui_switch_create(UiObject* obj, UiToggleArgs args) {
+    NSSwitch *button = [[NSSwitch alloc] init];
+    
+    UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args.value, args.varname, UI_VAR_INTEGER);
+    if(var) {
+        UiInteger *i = var->value;
+        i->obj = (__bridge void*)button;
+        i->get = ui_switch_get;
+        i->set = ui_switch_set;
+    }
+    
+    if(args.onchange) {
+        EventData *event = [[EventData alloc] init:args.onchange userdata:args.onchangedata];
+        event.get_eventdata = switch_eventdata;
+        event.obj = obj;
+        event.var = var;
+        event.vartype = UI_VAR_INTEGER;
+        button.target = event;
+        button.action = @selector(handleEventWithEventData:);
+        objc_setAssociatedObject(button, "eventdata", event, OBJC_ASSOCIATION_RETAIN);
+    }
+    
+    UiLayout layout = UI_INIT_LAYOUT(args);
+    ui_container_add(obj, button, &layout, FALSE);
+    
+    return (__bridge void*)button;
+}
+
+int64_t ui_switch_get(UiInteger *i) {
+    NSSwitch *button = (__bridge NSSwitch*)i->obj;
+    NSControlStateValue state = button.state;
+    i->value = (int64_t)state;
+    return (int64_t)state;
+}
+
+void ui_switch_set(UiInteger *i, int64_t value) {
+    NSSwitch *button = (__bridge NSSwitch*)i->obj;
+    NSControlStateValue state;
+    switch(value) {
+        case 0: state = NSControlStateValueOff; break;
+        case 1: state = NSControlStateValueOff; break;
+        default: state = NSControlStateValueMixed;
+    }
+    i->value = (int64_t)state;
+    button.state = state;
+}
+
+static void radiobutton_eventdata(id button, UiVar *var, void **eventdata, int *value) {
+    if(var) {
+        printf("switch radiobutton\n");
+    }
+}
+
+UIWIDGET ui_radiobutton_create(UiObject* obj, UiToggleArgs args) {
+    NSButton *button = [[NSButton alloc] init];
+    [button setButtonType:NSButtonTypeRadio];
+    
+    UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args.value, args.varname, UI_VAR_INTEGER);
+    NSMutableArray *buttons = nil;
+    if(var) {
+        UiInteger *i = var->value;
+        buttons = (__bridge NSMutableArray*)i->obj;
+        if(!buttons) {
+            buttons = [[NSMutableArray alloc] init];
+            i->obj = (__bridge void*)buttons;
+            i->get = ui_radiobuttons_get;
+            i->set = ui_radiobuttons_set;
+        }
+        [buttons addObject:button];
+        objc_setAssociatedObject(button, "radiogroup", buttons, OBJC_ASSOCIATION_RETAIN);
+    }
+    
+    if(args.onchange || var) {
+        EventData *event = [[EventData alloc] init:args.onchange userdata:args.onchangedata];
+        event.get_eventdata = radiobutton_eventdata;
+        event.obj = obj;
+        event.var = var;
+        event.vartype = UI_VAR_INTEGER;
+        button.target = event;
+        
+        
+        button.action = @selector(handleEventWithEventData:);
+        
+        objc_setAssociatedObject(button, "eventdata", event, OBJC_ASSOCIATION_RETAIN);
+    }
+    
+    UiLayout layout = UI_INIT_LAYOUT(args);
+    ui_container_add(obj, button, &layout, FALSE);
+    
+    return (__bridge void*)button;
+}
+
+int64_t ui_radiobuttons_get(UiInteger *i) {
+    return 0;
+}
+
+void ui_radiobuttons_set(UiInteger *i, int64_t value) {
+    
+}
--- a/ui/cocoa/window.m	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/cocoa/window.m	Tue Feb 25 21:11:00 2025 +0100
@@ -40,7 +40,7 @@
 #include <cx/mempool.h>
 
 static UiObject* create_window(const char *title, BOOL simple) {
-    CxMempool *mp = cxBasicMempoolCreate(256);
+    CxMempool *mp = cxMempoolCreateSimple(256);
     UiObject *obj = cxCalloc(mp->allocator, 1, sizeof(UiObject));
     obj->ref = 0;
     
--- a/ui/common/context.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/common/context.c	Tue Feb 25 21:11:00 2025 +0100
@@ -45,7 +45,7 @@
 static UiContext* global_context;
 
 void uic_init_global_context(void) {
-    CxMempool *mp = cxBasicMempoolCreate(32);
+    CxMempool *mp = cxMempoolCreateSimple(32);
     global_context = uic_context(NULL, mp);
 }
 
@@ -61,7 +61,7 @@
     ctx->obj = toplevel;
     ctx->vars = cxHashMapCreate(mp->allocator, CX_STORE_POINTERS, 16);
     
-    ctx->documents = cxLinkedListCreate(mp->allocator, cx_cmp_intptr, CX_STORE_POINTERS);
+    ctx->documents = cxLinkedListCreate(mp->allocator, cx_cmp_ptr, CX_STORE_POINTERS);
     ctx->group_widgets = cxLinkedListCreate(mp->allocator, cx_cmp_ptr, sizeof(UiGroupWidget));
     ctx->groups = cxArrayListCreate(mp->allocator, cx_cmp_int, sizeof(int), 32);
     
@@ -92,13 +92,14 @@
     ctx->document = document;
     
     UiContext *doc_ctx = ui_document_context(document);
+    doc_ctx->parent = ctx;
     
     // check if any parent context has an unbound variable with the same name
     // as any document variable
     UiContext *var_ctx = ctx;
     while(var_ctx) {
         if(var_ctx->vars_unbound &&  cxMapSize(var_ctx->vars_unbound) > 0) {
-            CxIterator i = cxMapIterator(var_ctx->vars_unbound);
+            CxMapIterator i = cxMapIterator(var_ctx->vars_unbound);
             cx_foreach(CxMapEntry*, entry, i) {
                 printf("attach %s\n", entry->key->data);
                 UiVar *var = entry->value;
@@ -111,15 +112,15 @@
             }
         }
         
-        var_ctx = ctx->parent;
+        var_ctx = var_ctx->parent;
     }
 }
 
 static void uic_context_unbind_vars(UiContext *ctx) {
-    CxIterator i = cxMapIterator(ctx->vars);
-    cx_foreach(CxMapEntry*, entry, i) {
+    CxMapIterator mi = cxMapIterator(ctx->vars);
+    cx_foreach(CxMapEntry*, entry, mi) {
         UiVar *var = entry->value;
-        if(var->from && var->from_ctx) {
+        if(var->from && var->from_ctx && var->from_ctx != ctx) {
             uic_save_var2(var);
             uic_copy_binding(var, var->from, FALSE);
             cxMapPut(var->from_ctx->vars_unbound, *entry->key, var->from);
@@ -128,7 +129,7 @@
     }
     
     if(ctx->documents) {
-        i = cxListIterator(ctx->documents);
+        CxIterator i = cxListIterator(ctx->documents);
         cx_foreach(void *, doc, i) {
             UiContext *subctx = ui_document_context(doc);
             uic_context_unbind_vars(subctx);
@@ -148,6 +149,7 @@
     
     UiContext *docctx = ui_document_context(document);
     uic_context_unbind_vars(docctx); // unbind all doc/subdoc vars from the parent
+    docctx->parent = NULL;
 }
 
 void uic_context_detach_all(UiContext *ctx) {
@@ -471,7 +473,7 @@
 
 
 void ui_set_group(UiContext *ctx, int group) {
-    if(cxListFind(ctx->groups, &group) == -1) {
+    if(!cxListIndexValid(ctx->groups, cxListFind(ctx->groups, &group))) {
         cxListAdd(ctx->groups, &group);
     }
     
--- a/ui/common/document.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/common/document.c	Tue Feb 25 21:11:00 2025 +0100
@@ -85,12 +85,7 @@
 void* ui_document_new(size_t size) {
     CxMempool *mp = cxMempoolCreate(256, NULL);
     const CxAllocator *a = mp->allocator;
-    UiContext *ctx = cxCalloc(a, 1, sizeof(UiContext));
-    ctx->mp = mp;
-    ctx->attach_document = uic_context_attach_document;
-    ctx->detach_document2 = uic_context_detach_document2;
-    ctx->allocator = a;
-    ctx->vars = cxHashMapCreate(a, CX_STORE_POINTERS, 16);
+    UiContext *ctx = uic_context(NULL, mp);
     
     void *document = cxCalloc(a, size, 1);
     cxMapPut(documents, cx_hash_key(&document, sizeof(void*)), ctx);
--- a/ui/common/types.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/common/types.c	Tue Feb 25 21:11:00 2025 +0100
@@ -231,6 +231,8 @@
     for (int i = 0; i < model->columns; i++) {
         newmodel->titles[i] = model->titles[i] ? cx_strdup_a(a, cx_str(model->titles[i])).ptr : NULL;
     }
+    newmodel->columnsize = cxCalloc(a, model->columns, sizeof(int));
+    memcpy(newmodel->columnsize, model->columnsize, model->columns*sizeof(int));
 
     return newmodel;
 }
@@ -239,6 +241,7 @@
     const CxAllocator* a = ctx ? ctx->allocator : cxDefaultAllocator;
     cxFree(a, mi->types);
     cxFree(a, mi->titles);
+    cxFree(a, mi->columnsize);
     cxFree(a, mi);
 }
 
@@ -301,8 +304,12 @@
 
 
 void ui_int_set(UiInteger* i, int64_t value) {
-    if (i && i->set) {
-        i->set(i, value);
+    if (i) {
+        if (i->set) {
+            i->set(i, value);
+        } else {
+            i->value = value;
+        }
     }
 }
 
@@ -315,8 +322,12 @@
 }
 
 void ui_double_set(UiDouble* d, double value) {
-    if (d && d->set) {
-        d->set(d, value);
+    if (d) {
+        if (d->set) {
+            d->set(d, value);
+        } else {
+            d->value = value;
+        }
     }
 }
 
@@ -330,8 +341,21 @@
 }
 
 void ui_string_set(UiString* s, const char* value) {
-    if (s && s->set) {
-        s->set(s, value);
+    if (s) {
+        if (s->set) {
+            s->set(s, value);
+        } else {
+            if(s->value.free) {
+                s->value.free(s->value.ptr);
+            }
+            if (value) {
+                s->value.ptr = strdup(value);
+                s->value.free = free;
+            } else {
+                s->value.ptr = NULL;
+                s->value.free = NULL;
+            }
+        }
     }
 }
 
@@ -345,8 +369,21 @@
 }
 
 void ui_text_set(UiText* s, const char* value) {
-    if (s && s->set) {
-        s->set(s, value);
+    if (s) {
+        if (s->set) {
+            s->set(s, value);
+        } else {
+            if(s->value.free) {
+                s->value.free(s->value.ptr);
+            }
+            if (value) {
+                s->value.ptr = strdup(value);
+                s->value.free = free;
+            } else {
+                s->value.ptr = NULL;
+                s->value.free = NULL;
+            }
+        }
     }
 }
 
--- a/ui/common/ucx_properties.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/common/ucx_properties.c	Tue Feb 25 21:11:00 2025 +0100
@@ -240,7 +240,7 @@
 }
 
 int ucx_properties_store(CxMap *map, FILE *file) {
-    CxIterator iter = cxMapIterator(map);
+    CxMapIterator iter = cxMapIterator(map);
     cxstring value;
     size_t written;
 
--- a/ui/gtk/container.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/gtk/container.c	Tue Feb 25 21:11:00 2025 +0100
@@ -52,6 +52,17 @@
     return 1;
 }
 
+UIEXPORT UIWIDGET ui_customwidget_create(UiObject *obj, ui_createwidget_func create_widget, void *userdata, UiWidgetArgs args) {
+    UiObject* current = uic_current_obj(obj);
+    
+    UIWIDGET widget = create_widget(obj, args, userdata);
+    
+    UI_APPLY_LAYOUT1(current, args);
+    current->container->add(current->container, widget, FALSE);
+    
+    return widget;
+}
+
 GtkWidget* ui_gtk_vbox_new(int spacing) {
 #if GTK_MAJOR_VERSION >= 3
     return gtk_box_new(GTK_ORIENTATION_VERTICAL, spacing);
@@ -96,7 +107,7 @@
         case UI_CONTAINER_GRID: {
             sub = ui_create_grid_widget(columnspacing, rowspacing);
             add = ui_box_set_margin(sub, margin);
-            newobj->container = ui_grid_container(newobj, sub);
+            newobj->container = ui_grid_container(newobj, sub, FALSE, FALSE, FALSE, FALSE);
             newobj->widget = sub;
             break;
         }
@@ -156,11 +167,22 @@
     ct->current = widget;
 }
 
-UiContainer* ui_grid_container(UiObject *obj, GtkWidget *grid) {
+UiContainer* ui_grid_container(
+        UiObject *obj,
+        GtkWidget *grid,
+        UiBool def_hexpand,
+        UiBool def_vexpand,
+        UiBool def_hfill,
+        UiBool def_vfill)
+{
     UiGridContainer *ct = cxCalloc(
             obj->ctx->allocator,
             1,
             sizeof(UiGridContainer));
+    ct->def_hexpand = def_hexpand;
+    ct->def_vexpand = def_vexpand;
+    ct->def_hfill = def_hfill;
+    ct->def_vfill = def_vfill;
     ct->container.widget = grid;
     ct->container.add = ui_grid_container_add;
     UI_GTK_V2(ct->width = 0);
@@ -182,15 +204,34 @@
     int vexpand = FALSE;
     int hfill = FALSE;
     int vfill = FALSE;
+    if(!ct->layout.override_defaults) {
+        if(grid->def_hexpand) {
+            hexpand = TRUE;
+            hfill = TRUE;
+        } else if(grid->def_hfill) {
+            hfill = TRUE;
+        }
+        if(grid->def_vexpand) {
+            vexpand = TRUE;
+            vfill = TRUE;
+        } else if(grid->def_vfill) {
+            vfill = TRUE;
+        }
+    }
+    
     if(ct->layout.fill != UI_LAYOUT_UNDEFINED) {
         fill = ui_lb2bool(ct->layout.fill);
     }
-    if(ct->layout.hexpand != UI_LAYOUT_UNDEFINED) {
-        hexpand = ct->layout.hexpand;
+    if(ct->layout.hexpand) {
+        hexpand = TRUE;
+        hfill = TRUE;
+    } else if(ct->layout.hfill) {
         hfill = TRUE;
     }
-    if(ct->layout.vexpand != UI_LAYOUT_UNDEFINED) {
-        vexpand = ct->layout.vexpand;
+    if(ct->layout.vexpand) {
+        vexpand = TRUE;
+        vfill = TRUE;
+    } else if(ct->layout.vfill) {
         vfill = TRUE;
     }
     if(fill) {
@@ -230,14 +271,57 @@
     
     int hexpand = FALSE;
     int vexpand = FALSE;
-    if(ct->layout.hexpand != UI_LAYOUT_UNDEFINED) {
-        hexpand = ct->layout.hexpand;
+    int hfill = FALSE;
+    int vfill = FALSE;
+    if(!ct->layout.override_defaults) {
+        if(grid->def_hexpand) {
+            hexpand = TRUE;
+            hfill = TRUE;
+        } else if(grid->def_hfill) {
+            hfill = TRUE;
+        }
+        if(grid->def_vexpand) {
+            vexpand = TRUE;
+            vfill = TRUE;
+        } else if(grid->def_vfill) {
+            vfill = TRUE;
+        }
+    }
+    
+    if(ct->layout.fill != UI_LAYOUT_UNDEFINED) {
+        fill = ui_lb2bool(ct->layout.fill);
+    }
+    if(ct->layout.hexpand) {
+        hexpand = TRUE;
+        hfill = TRUE;
+    } else if(ct->layout.hfill) {
+        hfill = TRUE;
     }
-    if(ct->layout.vexpand != UI_LAYOUT_UNDEFINED) {
-        vexpand = ct->layout.vexpand;
+    if(ct->layout.vexpand) {
+        vexpand = TRUE;
+        vfill = TRUE;
+    } else if(ct->layout.vfill) {
+        vfill = TRUE;
+    }
+    if(fill) {
+        hfill = TRUE;
+        vfill = TRUE;
     }
-    GtkAttachOptions xoptions = hexpand ? GTK_FILL | GTK_EXPAND : GTK_FILL;
-    GtkAttachOptions yoptions = vexpand ? GTK_FILL | GTK_EXPAND : GTK_FILL;
+    
+    GtkAttachOptions xoptions = 0;
+    GtkAttachOptions yoptions = 0;
+    if(hexpand) {
+        xoptions = GTK_EXPAND;
+    }
+    if(hfill) {
+        xoptions |= GTK_FILL;
+    }
+    if(vexpand) {
+        yoptions = GTK_EXPAND;
+    }
+    if(vfill) {
+        yoptions |= GTK_FILL;
+    }
     
     int colspan = ct->layout.colspan > 0 ? ct->layout.colspan : 1;
     int rowspan = ct->layout.rowspan > 0 ? ct->layout.rowspan : 1;
@@ -396,7 +480,7 @@
     current->container->add(current->container, widget, TRUE);
     
     UiObject *newobj = uic_object_new(obj, grid);
-    newobj->container = ui_grid_container(obj, grid);
+    newobj->container = ui_grid_container(obj, grid, args.def_hexpand, args.def_vexpand, args.def_hfill, args.def_vfill);
     uic_obj_add(obj, newobj);
     
     return widget;
@@ -755,7 +839,7 @@
         }
         case UI_CONTAINER_GRID: {
             sub = ui_create_grid_widget(data->columnspacing, data->rowspacing);
-            newobj->container = ui_grid_container(newobj, sub);
+            newobj->container = ui_grid_container(newobj, sub, FALSE, FALSE, FALSE, FALSE);
             break;
         }
     }
@@ -946,6 +1030,68 @@
 
 
 
+static UIWIDGET splitpane_create(UiObject *obj, UiOrientation orientation, UiSplitPaneArgs args) {
+    UiObject* current = uic_current_obj(obj);
+    
+    GtkWidget *pane0 = create_paned(orientation);
+    
+    UI_APPLY_LAYOUT1(current, args);
+    current->container->add(current->container, pane0, TRUE);
+    
+    int max = args.max_panes == 0 ? 2 : args.max_panes;
+    
+    UiObject *newobj = uic_object_new(obj, pane0);
+    newobj->container = ui_splitpane_container(obj, pane0, orientation, max);
+    uic_obj_add(obj, newobj);
+    
+    return pane0;
+}
+
+UIWIDGET ui_hsplitpane_create(UiObject *obj, UiSplitPaneArgs args) {
+    return splitpane_create(obj, UI_HORIZONTAL, args);
+}
+
+UIWIDGET ui_vsplitpane_create(UiObject *obj, UiSplitPaneArgs args) {
+    return splitpane_create(obj, UI_VERTICAL, args);
+}
+
+UiContainer* ui_splitpane_container(UiObject *obj, GtkWidget *pane, UiOrientation orientation, int max) {
+    UiSplitPaneContainer *ct = ui_calloc(obj->ctx, 1, sizeof(UiSplitPaneContainer));
+    ct->container.widget = pane;
+    ct->container.add = ui_splitpane_container_add;
+    ct->current_pane = pane;
+    ct->orientation = orientation;
+    ct->max = max;
+    return (UiContainer*)ct;
+}
+
+void ui_splitpane_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill) {
+    UiSplitPaneContainer *s = (UiSplitPaneContainer*)ct;
+    
+    if(s->nchildren >= s->max) {
+        fprintf(stderr, "splitpane: maximum number of children reached\n");
+        return;
+    }
+    
+    if(s->pos == 0) {
+        gtk_paned_set_start_child(GTK_PANED(s->current_pane), widget);
+        s->pos++;
+        s->nchildren++;
+    } else {
+        if(s->nchildren+1 == s->max) {
+            gtk_paned_set_end_child(GTK_PANED(s->current_pane), widget);
+        } else {
+            GtkWidget *pane = create_paned(s->orientation);
+            gtk_paned_set_start_child(GTK_PANED(pane), widget);
+            gtk_paned_set_end_child(GTK_PANED(s->current_pane), pane);
+            s->current_pane = pane;
+        }
+        
+        s->pos = 0;
+        s->nchildren++;
+    }
+}
+
 /* -------------------- ItemList Container -------------------- */
 
 static void remove_item(void *data, void *item) {
@@ -997,7 +1143,7 @@
             ui_box_container_add(ct->container, item_obj->widget, FALSE);
         } else {
             // create new widget and object for this list element
-            CxMempool *mp = cxBasicMempoolCreate(256);
+            CxMempool *mp = cxMempoolCreateSimple(256);
             const CxAllocator *a = mp->allocator;
             UiObject *obj = cxCalloc(a, 1, sizeof(UiObject));
             obj->ctx = uic_context(obj, mp);
@@ -1102,6 +1248,11 @@
     ct->layout.vfill = fill;
 }
 
+UIEXPORT void ui_layout_override_defaults(UiObject *obj, UiBool d) {
+    UiContainer *ct = uic_get_current_container(obj);
+    ct->layout.override_defaults = d;
+}
+
 void ui_layout_colspan(UiObject* obj, int cols) {
     UiContainer* ct = uic_get_current_container(obj);
     ct->layout.colspan = cols;
--- a/ui/gtk/container.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/gtk/container.h	Tue Feb 25 21:11:00 2025 +0100
@@ -31,6 +31,7 @@
 
 #include "../ui/toolkit.h"
 #include "../ui/container.h"
+#include "toolkit.h"
 #include <string.h>
 
 #include <cx/allocator.h>
@@ -65,6 +66,7 @@
     UiBool       vexpand;
     UiBool       hfill;
     UiBool       vfill;
+    UiBool       override_defaults;
     int          width;
     int          colspan;
     int          rowspan;
@@ -89,6 +91,10 @@
 
 typedef struct UiGridContainer {
     UiContainer container;
+    UiBool def_hexpand;
+    UiBool def_vexpand;
+    UiBool def_hfill;
+    UiBool def_vfill;
     int x;
     int y;
 #ifdef UI_GTK2
@@ -97,16 +103,6 @@
 #endif
 } UiGridContainer;
 
-/*
-typedef struct UiPanedContainer {
-    UiContainer container;
-    GtkWidget *current_pane;
-    int orientation;
-    int max;
-    int cur;
-} UiPanedContainer;
-*/
-
 typedef struct UiTabViewContainer {
     UiContainer container;
 } UiTabViewContainer;
@@ -127,6 +123,16 @@
     int rowspacing;
 } UiGtkTabView;
 
+
+typedef struct UiSplitPaneContainer {
+    UiContainer container;
+    GtkWidget *current_pane;
+    UiOrientation orientation;
+    int pos;
+    int max;
+    int nchildren;
+} UiSplitPaneContainer;
+
 typedef struct UiHeaderbarContainer {
     UiContainer container;
     GtkWidget *centerbox;
@@ -170,7 +176,13 @@
 void ui_box_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
 
 GtkWidget* ui_create_grid_widget(int colspacing, int rowspacing);
-UiContainer* ui_grid_container(UiObject *obj, GtkWidget *grid);
+UiContainer* ui_grid_container(
+        UiObject *obj,
+        GtkWidget *grid,
+        UiBool def_hexpand,
+        UiBool def_vexpand,
+        UiBool def_hfill,
+        UiBool def_vfill);
 void ui_grid_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
 
 UiContainer* ui_frame_container(UiObject *obj, GtkWidget *frame);
@@ -185,10 +197,9 @@
 UiContainer* ui_tabview_container(UiObject *obj, GtkWidget *tabview);
 void ui_tabview_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
 
-void ui_paned_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
+UiContainer* ui_splitpane_container(UiObject *obj, GtkWidget *pane, UiOrientation orientation, int max);
+void ui_splitpane_container_add(UiContainer *ct, GtkWidget *widget, UiBool fill);
 
-void ui_split_container_add1(UiContainer *ct, GtkWidget *widget, UiBool fill);
-void ui_split_container_add2(UiContainer *ct, GtkWidget *widget, UiBool fill);
 
 UiGtkTabView* ui_widget_get_tabview_data(UIWIDGET tabview);
 
--- a/ui/gtk/list.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/gtk/list.c	Tue Feb 25 21:11:00 2025 +0100
@@ -388,6 +388,13 @@
         GtkColumnViewColumn *column = gtk_column_view_column_new(model->titles[i], factory);
         gtk_column_view_column_set_resizable(column, true);
         gtk_column_view_append_column(GTK_COLUMN_VIEW(view), column);
+        
+        int size = model->columnsize[i];
+        if(size > 0) {
+            gtk_column_view_column_set_fixed_width(column, size);
+        } else if(size < 0) {
+            gtk_column_view_column_set_expand(column, TRUE);
+        }
     }
     
     // bind listview to list
@@ -447,13 +454,7 @@
     }
 }
 
-void ui_columnview_activate(void *ignore, guint position, gpointer userdata) {
-    UiListView *view = userdata;
-    listview_event(view->onactivate, view->onactivatedata, view);
-}
-
-void ui_listview_selection_changed(GtkSelectionModel* self, guint position, guint n_items, gpointer userdata) {
-    UiListView *view = userdata;
+static void listview_update_selection(UiListView *view) {
     free(view->selection.rows);
     view->selection.count = 0;
     view->selection.rows = NULL;
@@ -476,7 +477,19 @@
     } else {
         free(newselection);
     }
+}
     
+void ui_columnview_activate(void *ignore, guint position, gpointer userdata) {
+    UiListView *view = userdata;
+    if(view->selection.count == 0) {
+        listview_update_selection(view);
+    }
+    listview_event(view->onactivate, view->onactivatedata, view);
+}
+
+void ui_listview_selection_changed(GtkSelectionModel* self, guint position, guint n_items, gpointer userdata) {
+    UiListView *view = userdata;
+    listview_update_selection(view);
     listview_event(view->onselection, view->onselectiondata, view);
 }
 
@@ -513,7 +526,7 @@
 
 void ui_listview_update2(UiList *list, int i) {
     UiListView *view = list->obj;
-    ui_update_liststore(view->liststore, view->var->value);
+    ui_update_liststore(view->liststore, list);
 }
 
 UiListSelection ui_listview_getselection2(UiList *list) {
@@ -614,8 +627,8 @@
                     }
                     case UI_INTEGER: {
                         g_value_init(&value, G_TYPE_INT);
-                        int *intptr = data;
-                        g_value_set_int(&value, *intptr);
+                        intptr_t intptr = (intptr_t)data;
+                        g_value_set_int(&value, (int)intptr);
                         break;
                     }
                     case UI_ICON: {
@@ -1559,6 +1572,34 @@
 }
 #endif
 
+
+static void add_sublist(UiListBox *uilistbox, CxList *sublists, UiSubList *sublist) {
+    UiListBoxSubList uisublist;
+    uisublist.var = uic_widget_var(
+            uilistbox->obj->ctx,
+            uilistbox->obj->ctx,
+            sublist->value,
+            sublist->varname,
+            UI_VAR_LIST);
+    uisublist.numitems = 0;
+    uisublist.header = sublist->header ? strdup(sublist->header) : NULL;
+    uisublist.separator = sublist->separator;
+    uisublist.widgets = cxLinkedListCreateSimple(CX_STORE_POINTERS);
+    uisublist.listbox = uilistbox;
+    uisublist.userdata = sublist->userdata;
+    uisublist.index = cxListSize(sublists);
+
+    // bind UiList
+    UiListBoxSubList *sublist_ptr = cxListAt(uilistbox->sublists, cxListSize(sublists)-1);
+    UiList *list = uisublist.var->value;
+    if(list) {
+        list->obj = sublist_ptr;
+        list->update = ui_listbox_list_update;
+    }
+
+    cxListAdd(sublists, &uisublist);
+}
+
 UIEXPORT UIWIDGET ui_sourcelist_create(UiObject *obj, UiSourceListArgs args) {
     UiObject* current = uic_current_obj(obj);
     
@@ -1596,42 +1637,32 @@
     uilistbox->sublists->collection.destructor_data = obj;
     uilistbox->first_row = NULL;
     
-    if(args.numsublists == 0 && args.sublists) {
-        args.numsublists = INT_MAX;
-    }
-    for(int i=0;i<args.numsublists;i++) {
-        UiSubList sublist = args.sublists[i];
-        if(!sublist.varname && !sublist.value) {
-            break;
+    if(args.sublists) {
+        // static sublist initalization
+        if(args.numsublists == 0 && args.sublists) {
+            args.numsublists = INT_MAX;
+        }
+        for(int i=0;i<args.numsublists;i++) {
+            UiSubList sublist = args.sublists[i];
+            if(!sublist.varname && !sublist.value) {
+                break;
+            }
+            
+            add_sublist(uilistbox, uilistbox->sublists, &sublist);
         }
         
-        UiListBoxSubList uisublist;
-        uisublist.var = uic_widget_var(
-                obj->ctx,
-                current->ctx,
-                sublist.value,
-                sublist.varname,
-                UI_VAR_LIST);
-        uisublist.numitems = 0;
-        uisublist.header = sublist.header ? strdup(sublist.header) : NULL;
-        uisublist.separator = sublist.separator;
-        uisublist.widgets = cxLinkedListCreateSimple(CX_STORE_POINTERS);
-        uisublist.listbox = uilistbox;
-        uisublist.userdata = sublist.userdata;
-        uisublist.index = i;
-        
-        cxListAdd(uilistbox->sublists, &uisublist);
-        
-        // bind UiList
-        UiListBoxSubList *sublist_ptr = cxListAt(uilistbox->sublists, cxListSize(uilistbox->sublists)-1);
-        UiList *list = uisublist.var->value;
-        if(list) {
-            list->obj = sublist_ptr;
-            list->update = ui_listbox_list_update;
+        // fill items
+        ui_listbox_update(uilistbox, 0, cxListSize(uilistbox->sublists));
+    } else {
+        UiVar* var = uic_widget_var(obj->ctx, current->ctx, args.dynamic_sublist, args.varname, UI_VAR_LIST);
+        if(var) {
+            UiList *list = var->value;
+            list->obj = uilistbox;
+            list->update = ui_listbox_dynamic_update;
+            
+            ui_listbox_dynamic_update(list, 0);
         }
     }
-    // fill items
-    ui_listbox_update(uilistbox, 0, cxListSize(uilistbox->sublists));
     
     // register uilistbox for both widgets, so it doesn't matter which
     // widget is used later
@@ -1656,6 +1687,35 @@
     return scroll_area;
 }
 
+void ui_listbox_dynamic_update(UiList *list, int x) {
+    UiListBox *uilistbox = list->obj;
+    
+    // unbind/free previous list vars
+    CxIterator i = cxListIterator(uilistbox->sublists);
+    cx_foreach(UiListBoxSubList *, s, i) {
+        if(s->var) {
+            UiList *sl = s->var->value;
+            sl->obj = NULL;
+            sl->update = NULL;
+            if(s->var->type == UI_VAR_SPECIAL) {
+                ui_free(s->var->from_ctx, s->var);
+            }
+        }
+    }
+    
+    cxListFree(uilistbox->sublists);
+    CxList *new_sublists = cxArrayListCreateSimple(sizeof(UiListBoxSubList), list->count(list));
+    uilistbox->sublists = new_sublists;
+    
+    UiSubList *sublist = list->first(list);
+    while(sublist) {
+        add_sublist(uilistbox, new_sublists, sublist);
+        sublist = list->next(list);
+    }
+    
+    ui_listbox_update(uilistbox, 0, cxListSize(uilistbox->sublists));
+}
+
 void ui_listbox_update(UiListBox *listbox, int from, int to) {
     CxIterator i = cxListIterator(listbox->sublists);
     size_t pos = 0;
@@ -1777,11 +1837,19 @@
     }
     UiListBoxSubList *sublist = data->customdata0;
     
+    UiSubListEventData eventdata;
+    eventdata.list = sublist->var->value;
+    eventdata.sublist_index = sublist->index;
+    eventdata.row_index = data->value0;
+    eventdata.sublist_userdata = sublist->userdata;
+    eventdata.row_data = ui_list_get(eventdata.list, eventdata.row_index);
+    eventdata.event_data = data->customdata2;
+    
     UiEvent event;
     event.obj = data->obj;
     event.window = event.obj->window;
     event.document = event.obj->ctx->document;
-    event.eventdata = data->customdata2;
+    event.eventdata = &eventdata;
     event.intval = data->value0;
     
     if(data->callback) {
--- a/ui/gtk/list.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/gtk/list.h	Tue Feb 25 21:11:00 2025 +0100
@@ -161,6 +161,7 @@
 UiListSelection ui_combobox_getselection(UiList *list);
 void ui_combobox_setselection(UiList *list, UiListSelection selection);
 
+void ui_listbox_dynamic_update(UiList *list, int i);
 void ui_listbox_update(UiListBox *listbox, int from, int to);
 void ui_listbox_update_sublist(UiListBox *listbox, UiListBoxSubList *sublist, size_t listbox_insert_index);
 void ui_listbox_list_update(UiList *list, int i);
--- a/ui/gtk/objs.mk	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/gtk/objs.mk	Tue Feb 25 21:11:00 2025 +0100
@@ -46,6 +46,7 @@
 GTKOBJ += entry.o
 GTKOBJ += dnd.o
 GTKOBJ += headerbar.o
+GTKOBJ += webview.o
 
 TOOLKITOBJS += $(GTKOBJ:%=$(GTK_OBJPRE)%)
 TOOLKITSOURCE += $(GTKOBJ:%.o=gtk/%.c)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/gtk/webview.c	Tue Feb 25 21:11:00 2025 +0100
@@ -0,0 +1,177 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "toolkit.h"
+#include "container.h"
+
+#include "webview.h"
+
+#ifdef UI_WEBVIEW
+
+UIWIDGET ui_webview_create(UiObject *obj, UiWebviewArgs args) {
+    UiObject* current = uic_current_obj(obj);
+    
+    GtkWidget *webview = webkit_web_view_new();
+    
+    ui_set_name_and_style(webview, args.name, args.style_class);
+    
+    UiVar *var = uic_widget_var(obj->ctx, current->ctx, args.value, args.varname, UI_VAR_GENERIC);
+    if(var) {
+        WebViewData *data = malloc(sizeof(WebViewData));
+        memset(data, 0, sizeof(WebViewData));
+        data->webview = WEBKIT_WEB_VIEW(webview);
+        WebKitSettings *settings = webkit_web_view_get_settings(data->webview);
+        data->javascript = webkit_settings_get_enable_javascript(settings);
+        data->zoom = webkit_web_view_get_zoom_level(data->webview);
+        
+        UiGeneric *value = var->value;
+        value->get = ui_webview_get;
+        value->get_type = ui_webview_get_type;
+        value->set = ui_webview_set;
+        value->obj = data;
+        if(value->value && value->type && !strcmp(value->type, UI_WEBVIEW_OBJECT_TYPE)) {
+            // TODO
+        }
+    }
+    
+    ui_set_widget_groups(obj->ctx, webview, args.groups);
+    UI_APPLY_LAYOUT1(current, args);
+    current->container->add(current->container, webview, FALSE);
+    
+    return webview;
+}
+
+void* ui_webview_get(UiGeneric *g) {
+    return g->value;
+}
+
+const char* ui_webview_get_type(UiGeneric *g) {
+    return UI_WEBVIEW_OBJECT_TYPE;
+}
+
+int ui_webview_set(UiGeneric *g, void *value, const char *type) {
+    if(!type || strcmp(type, UI_WEBVIEW_OBJECT_TYPE)) {
+        return 1;
+    }
+    
+    WebViewData *obj = g->obj;
+    if(!obj->webview) {
+        return 1;
+    }
+    
+    WebViewData *data = value;
+    if(data->type == WEBVIEW_CONTENT_URL) {
+        webkit_web_view_load_uri(obj->webview, data->uri);
+    } else {
+        if(!data->content) {
+            return 1;
+        }
+        
+        GBytes *bytes = g_bytes_new(data->content, data->contentlength);
+        webkit_web_view_load_bytes(obj->webview, bytes, data->mimetype, data->encoding, data->uri);
+    }
+    
+    ui_webview_enable_javascript(g, data->javascript);
+    webkit_web_view_set_zoom_level(data->webview, data->zoom);
+    
+    return 0;
+}
+
+void ui_webview_load_url(UiGeneric *g, const char *url) {
+    WebViewData data = { .uri = (char*)url, .type = WEBVIEW_CONTENT_URL };
+    g->set(g, &data, UI_WEBVIEW_OBJECT_TYPE);
+}
+
+void ui_webview_load_content(
+        UiGeneric *g,
+        const char *uri,
+        const char *content,
+        size_t contentlength,
+        const char *mimetype,
+        const char *encoding)
+{
+    WebViewData data;
+    data.uri = (char*)uri;
+    data.content = (char*)content;
+    data.contentlength = contentlength;
+    data.mimetype = (char*)mimetype;
+    data.encoding = (char*)encoding;
+    data.type = WEBVIEW_CONTENT_CONTENT;
+    g->set(g, &data, UI_WEBVIEW_OBJECT_TYPE);
+}
+
+void ui_webview_reload(UiGeneric *g) {
+    WebViewData *webview = g->obj;
+    webkit_web_view_reload(webview->webview);
+}
+
+UiBool ui_webview_can_go_back(UiGeneric *g) {
+    WebViewData *webview = g->obj;
+    return webkit_web_view_can_go_back(webview->webview);
+}
+
+UiBool ui_webview_can_go_forward(UiGeneric *g) {
+    WebViewData *webview = g->obj;
+    return webkit_web_view_can_go_forward(webview->webview);
+}
+
+void ui_webview_go_back(UiGeneric *g) {
+    WebViewData *webview = g->obj;
+    webkit_web_view_go_back(webview->webview);
+}
+
+void ui_webview_go_forward(UiGeneric *g) {
+    WebViewData *webview = g->obj;
+    webkit_web_view_go_forward(webview->webview);
+}
+
+const char* ui_webview_get_uri(UiGeneric *g) {
+    WebViewData *webview = g->obj;
+    return webkit_web_view_get_uri(webview->webview);
+}
+
+void ui_webview_enable_javascript(UiGeneric *g, UiBool enable) {
+    WebViewData *webview = g->obj;
+    WebKitSettings *settings = webkit_web_view_get_settings(webview->webview);
+    webkit_settings_set_enable_javascript(settings, enable);
+}
+
+void ui_webview_set_zoom(UiGeneric *g, double zoom) {
+    WebViewData *webview = g->obj;
+    webkit_web_view_set_zoom_level(webview->webview, zoom);
+    webview->zoom = zoom;
+}
+
+double ui_webview_get_zoom(UiGeneric *g) {
+    WebViewData *webview = g->obj;
+    webview->zoom = webkit_web_view_get_zoom_level(webview->webview);
+    return webview->zoom;
+}
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/gtk/webview.h	Tue Feb 25 21:11:00 2025 +0100
@@ -0,0 +1,70 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef WEBVIEW_H
+#define WEBVIEW_H
+
+#ifdef UI_WEBVIEW
+
+#include "../ui/webview.h"
+#include <webkit/webkit.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+    
+enum WebViewDataType {
+    WEBVIEW_CONTENT_URL,
+    WEBVIEW_CONTENT_CONTENT
+};
+    
+typedef struct WebViewData {
+    WebKitWebView *webview;
+    char *uri;
+    char *mimetype;
+    char *encoding;
+    char *content;
+    size_t contentlength;
+    enum WebViewDataType type;
+    
+    double zoom;
+    UiBool javascript;
+} WebViewData;
+
+void* ui_webview_get(UiGeneric *g);
+const char* ui_webview_get_type(UiGeneric *g);
+int ui_webview_set(UiGeneric *g, void *value, const char *type);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UI_WEBVIEW */
+
+#endif /* WEBVIEW_H */
+
--- a/ui/gtk/window.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/gtk/window.c	Tue Feb 25 21:11:00 2025 +0100
@@ -102,7 +102,7 @@
 #endif
 
 static UiObject* create_window(const char *title, void *window_data, UiBool sidebar, UiBool simple) {
-    CxMempool *mp = cxBasicMempoolCreate(256);
+    CxMempool *mp = cxMempoolCreateSimple(256);
     UiObject *obj = cxCalloc(mp->allocator, 1, sizeof(UiObject));
     obj->ref = 0;
    
@@ -168,13 +168,14 @@
     GtkWidget *content_box = ui_gtk_vbox_new(0);
     BOX_ADD_EXPAND(GTK_BOX(vbox), content_box);
     
+    GtkWidget *sidebar_headerbar = NULL; 
     if(sidebar) {
         GtkWidget *splitview = adw_overlay_split_view_new();
         adw_application_window_set_content(ADW_APPLICATION_WINDOW(obj->widget), splitview);
         
         GtkWidget *sidebar_toolbar_view = adw_toolbar_view_new();
         adw_overlay_split_view_set_sidebar(ADW_OVERLAY_SPLIT_VIEW(splitview), sidebar_toolbar_view);
-        GtkWidget *sidebar_headerbar = adw_header_bar_new();
+        sidebar_headerbar = adw_header_bar_new();
         adw_toolbar_view_add_top_bar(ADW_TOOLBAR_VIEW(sidebar_toolbar_view), sidebar_headerbar);
         
         adw_overlay_split_view_set_content(ADW_OVERLAY_SPLIT_VIEW(splitview), toolbar_view);
@@ -184,8 +185,25 @@
         adw_application_window_set_content(ADW_APPLICATION_WINDOW(obj->widget), toolbar_view);
     }
     
-
     GtkWidget *headerbar = adw_header_bar_new();
+    
+    const char *show_title = ui_get_property("ui.gtk.window.showtitle");
+    if(show_title) {
+        if(!strcmp(show_title, "main") && sidebar) {
+            adw_header_bar_set_show_title(ADW_HEADER_BAR(sidebar_headerbar), FALSE);
+        } else if(!strcmp(show_title, "sidebar")) {
+            adw_header_bar_set_show_title(ADW_HEADER_BAR(headerbar), FALSE);
+        } else if(!strcmp(show_title, "false")) {
+            adw_header_bar_set_show_title(ADW_HEADER_BAR(sidebar_headerbar), FALSE);
+            adw_header_bar_set_show_title(ADW_HEADER_BAR(headerbar), FALSE);
+        } else {
+            fprintf(stderr, "Unknown value '%s' for property ui.gtk.window.showtitle\n", show_title);
+            adw_header_bar_set_show_title(ADW_HEADER_BAR(sidebar_headerbar), FALSE);
+        }
+    } else {
+        adw_header_bar_set_show_title(ADW_HEADER_BAR(headerbar), FALSE);
+    }
+    
     adw_toolbar_view_add_top_bar(ADW_TOOLBAR_VIEW(toolbar_view), headerbar);
     g_object_set_data(G_OBJECT(obj->widget), "ui_headerbar", headerbar);
     
@@ -743,7 +761,7 @@
         gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
     }
     
-    CxMempool *mp = cxBasicMempoolCreate(256);
+    CxMempool *mp = cxMempoolCreateSimple(256);
     UiObject *obj = cxCalloc(mp->allocator, 1, sizeof(UiObject)); 
     obj->ctx = uic_context(obj, mp);
     obj->widget = dialog;
--- a/ui/motif/Grid.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/motif/Grid.c	Tue Feb 25 21:11:00 2025 +0100
@@ -548,6 +548,8 @@
             Dimension actual_width, actual_height;
             w->mywidget.sizerequest = TRUE;
             
+            //printf("sizerequest: %d x %d\n", (int)req_width, (int)req_height);
+            
             //XtWidgetGeometry request;
             //request.width = req_width;
             //request.request_mode = CWWidth;
--- a/ui/motif/container.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/motif/container.c	Tue Feb 25 21:11:00 2025 +0100
@@ -34,8 +34,26 @@
 #include "../common/context.h"
 #include "../common/object.h"
 
+#include <cx/array_list.h>
+
 #include "Grid.h"
 
+
+UIWIDGET ui_customwidget_create(UiObject *obj, ui_createwidget_func create_widget, void *userdata, UiWidgetArgs args) {
+    Arg xargs[64];
+    int n = 0;
+    
+    UiContainerPrivate *ctn = ui_obj_container(obj);
+    UI_APPLY_LAYOUT(ctn->layout, args);
+    
+    Widget parent = ctn->prepare(ctn, xargs, &n);
+    Widget widget = create_widget(obj, args, userdata, parent, xargs, n);
+    XtManageChild(widget);
+    ctn->add(ctn, widget);
+    
+    return widget;
+}
+
 /* ---------------------------- Box Container ---------------------------- */
 
 static UIWIDGET box_create(UiObject *obj, UiContainerArgs args, UiBoxOrientation orientation) { 
@@ -203,6 +221,344 @@
 }
 
 
+/* -------------------------- TabView Container -------------------------- */
+
+static void ui_tabbar_resize(Widget widget, XtPointer udata, XtPointer cdata) {
+    UiMotifTabView *tabview = udata;
+    
+    if(tabview->tabview == UI_TABVIEW_INVISIBLE) {
+        return;
+    }
+    
+    int width = 0;
+    int height = 0;
+    XtVaGetValues(widget, XmNwidth, &width, XmNheight, &height, NULL);
+    int numbuttons = cxListSize(tabview->tabs);
+    if(numbuttons == 0) {
+        return;
+    }
+    int button_width = width / numbuttons;
+    int x = 0;
+    
+    CxIterator i = cxListIterator(tabview->tabs);
+    cx_foreach(UiTab *, tab, i) {
+        if(i.index + 1 == numbuttons) {
+            button_width = width - x;
+        }
+        XtVaSetValues(
+                tab->tab_button,
+                XmNx, x,
+                XmNy, 0,
+                XmNwidth,
+                button_width,
+                
+                NULL);
+        x += button_width;
+    }
+    
+    if(height <= tabview->height) {
+        XtVaSetValues(widget, XmNheight, tabview->height + 4, NULL);
+    }
+}
+
+static void ui_tabbar_expose(Widget widget, XtPointer udata, XtPointer cdata) {
+    UiMotifTabView *tabview = udata;
+    XmDrawingAreaCallbackStruct *cbs = (XmDrawingAreaCallbackStruct *)cdata;
+    XEvent *event = cbs->event;
+    Display *dpy = XtDisplay(widget); 
+    
+    if(!tabview->gc_initialized) {
+        XGCValues gcvals;
+        gcvals.foreground = tabview->fg1;
+        tabview->gc = XCreateGC(XtDisplay(tabview->tabbar), XtWindow(tabview->tabbar), (GCForeground), &gcvals);
+    }
+    
+    if(tabview->current_tab) {
+        Widget tab = tabview->current_tab->tab_button;
+        XFillRectangle(dpy, XtWindow(widget), tabview->gc, tab->core.x, tab->core.height, tab->core.width, 4);
+    }
+}
+
+UIWIDGET ui_tabview_create(UiObject *obj, UiTabViewArgs args) {
+    Arg xargs[16];
+    int n = 0;
+    
+    UiContainerPrivate *ctn = ui_obj_container(obj);
+    UI_APPLY_LAYOUT(ctn->layout, args);
+    
+    // create widgets
+    // form
+    // - tabbar  (Drawing Area)
+    // - content (Frame)
+    UiMotifTabView *tabview = malloc(sizeof(UiMotifTabView));
+    memset(tabview, 0, sizeof(UiMotifTabView));
+    char *name = args.name ? (char*)args.name : "tabview";
+    XtSetArg(xargs[n], XmNuserData, tabview); n++;
+    Widget parent = ctn->prepare(ctn, xargs, &n);
+    Widget form = XmCreateForm(parent, name, xargs, n);
+    XtManageChild(form);
+    
+    n = 0;
+    XtSetArg(xargs[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(xargs[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(xargs[n], XmNtopAttachment, XmATTACH_FORM); n++;
+    XtSetArg(xargs[n], XmNorientation, XmHORIZONTAL); n++;
+    XtSetArg(xargs[n], XmNpacking, XmPACK_TIGHT); n++;
+    XtSetArg(xargs[n], XmNspacing, 1); n++;
+    XtSetArg(xargs[n], XmNmarginWidth, 0); n++;
+    XtSetArg(xargs[n], XmNmarginHeight, 0); n++;
+    Widget tabbar = XmCreateDrawingArea(form, "ui_test", xargs, n);
+    XtManageChild(tabbar);
+    XtAddCallback(tabbar, XmNresizeCallback , ui_tabbar_resize, tabview);
+    XtAddCallback(tabbar, XmNexposeCallback, ui_tabbar_expose, tabview);
+    
+    n = 0;
+    XtSetArg(xargs[n], XmNleftAttachment, XmATTACH_FORM); n++;
+    XtSetArg(xargs[n], XmNrightAttachment, XmATTACH_FORM); n++;
+    XtSetArg(xargs[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+    XtSetArg(xargs[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
+    XtSetArg(xargs[n], XmNtopWidget, tabbar); n++;
+    Widget content = XmCreateFrame(form, "tabviewcontent", xargs, n);
+    
+    // setup tabview object, that holds all relevant objects
+    tabview->obj = obj;
+    tabview->form = form;
+    tabview->tabbar = tabbar;
+    tabview->content = content;
+    tabview->tabview = args.tabview;
+    tabview->subcontainer = args.subcontainer;
+    tabview->select = ui_motif_tabview_select;
+    tabview->add = ui_motif_tabview_add_tab;
+    tabview->remove = ui_motif_tabview_remove;
+    tabview->tabs = cxArrayListCreate(obj->ctx->allocator, cx_cmp_ptr, sizeof(UiTab), 8);
+    tabview->current_index = -1;
+    
+    UiTabViewContainer *ct = ui_malloc(obj->ctx, sizeof(UiTabViewContainer));
+    ct->container.widget = form;
+    ct->container.type = UI_CONTAINER_TABVIEW;
+    ct->container.prepare = ui_tabview_container_prepare;
+    ct->container.add = ui_tabview_container_add;
+    ct->tabview = tabview;
+    
+    UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args.value, args.varname, UI_VAR_INTEGER);
+    if(var) {
+        UiInteger *i = var->value;
+        i->obj = tabview;
+        i->get = ui_tabview_get;
+        i->set = ui_tabview_set;
+    }
+    
+    uic_object_push_container(obj, (UiContainerX*)ct);
+    
+    return form;
+}
+
+int64_t ui_tabview_get(UiInteger *i) {
+    UiMotifTabView *tabview = i->obj;
+    i->value = tabview->current_index;
+    return i->value;
+}
+
+void ui_tabview_set(UiInteger *i, int64_t value) {
+    UiMotifTabView *tabview = i->obj;
+    if(value < cxListSize(tabview->tabs)) {
+        ui_motif_tabview_select(tabview, value);
+        i->value = value;
+    }
+}
+
+void ui_tab_create(UiObject *obj, const char* title) {
+    UiContainerPrivate *ctn = ui_obj_container(obj);
+    if(ctn->type != UI_CONTAINER_TABVIEW) {
+        fprintf(stderr, "UI Error: container is not a tabview\n");
+        return;
+    }
+    
+    UiMotifTabView *tabview = NULL;
+    XtVaGetValues(ctn->widget, XmNuserData, &tabview, NULL);
+    if(!tabview) {
+        fprintf(stderr, "UI Error: no tabview\n");
+        return;
+    }
+    
+    
+    Widget child = ui_vbox_create(obj, (UiContainerArgs) { 0 });
+    if(tabview->current) {
+        XtUnmanageChild(child);
+    } else {
+        tabview->current = child;
+    }
+    
+    tabview->add(tabview, -1, title, child);
+}
+
+void ui_tabview_select(UIWIDGET tabview, int tab) {
+    UiMotifTabView *tabviewdata = NULL;
+    XtVaGetValues(tabview, XmNuserData, &tabviewdata, NULL);
+    if(tabviewdata) {
+        ui_motif_tabview_select(tabviewdata, tab);
+    } else {
+        fprintf(stderr, "ui_tabview_select: widget is not a tabview\n");
+    }
+}
+
+void ui_tabview_remove(UIWIDGET tabview, int tab) {
+    UiMotifTabView *tabviewdata = NULL;
+    XtVaGetValues(tabview, XmNuserData, &tabviewdata, NULL);
+    if(tabviewdata) {
+        ui_motif_tabview_remove(tabviewdata, tab);
+    } else {
+        fprintf(stderr, "ui_tabview_select: widget is not a tabview\n");
+    }
+}
+
+UiObject* ui_tabview_add(UIWIDGET tabview, const char *name, int tab_index) {
+    UiMotifTabView *tabviewdata = NULL;
+    XtVaGetValues(tabview, XmNuserData, &tabviewdata, NULL);
+    if(tabviewdata) {
+        Arg args[16];
+        int n = 0;
+        
+        XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
+        XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
+        XtSetArg(args[n], XmNtopWidget, tabviewdata->tabbar); n++;
+        
+        Widget grid = XtCreateManagedWidget("vbox", gridClass, tabviewdata->content, args, n);
+        
+        UiObject *newobj = ui_calloc(tabviewdata->obj->ctx, 1, sizeof(UiObject));
+        newobj->ctx = tabviewdata->obj->ctx;
+        newobj->widget = grid;
+        UiContainerX *container = ui_box_container(newobj, grid, UI_BOX_VERTICAL);
+        newobj->container_begin = container;
+        newobj->container_end = container;
+        return newobj;
+    } else {
+        fprintf(stderr, "ui_tabview_select: widget is not a tabview\n");
+        return NULL;
+    }
+}
+
+void ui_motif_tabview_select(UiMotifTabView *tabview, int tab) {
+    UiTab *t = cxListAt(tabview->tabs, tab);
+    if(t) {
+        tabview->current_index = tab;
+        ui_motif_tabview_change_tab(tabview, t);
+    }
+}
+
+static void ui_tab_button_callback(Widget widget, UiTab *tab, XtPointer d) {
+    UiMotifTabView *tabview = NULL;
+    XtVaGetValues(widget, XmNuserData, &tabview, NULL);
+    ui_motif_tabview_change_tab(tabview, tab);
+}
+
+void ui_motif_tabview_add_tab(UiMotifTabView *tabview, int index, const char *name, Widget child) {
+    UiTab tab;
+    
+    Arg args[16];
+    int n = 0;
+    
+    XmString label = XmStringCreateLocalized((char*)name);
+    XtSetArg(args[n], XmNlabelString, label); n++;
+    XtSetArg(args[n], XmNshadowThickness, 0); n++;
+    XtSetArg(args[n], XmNhighlightThickness, 0); n++;
+    XtSetArg(args[n], XmNuserData, tabview); n++;
+    
+    Widget button = XmCreatePushButton(tabview->tabbar, "tab_button", args, n);
+    if(tabview->tabview != UI_TABVIEW_INVISIBLE) {
+        XtManageChild(button);
+    }
+    
+    if(tabview->height == 0) {
+        Dimension h;
+        XtVaGetValues(
+                button,
+                XmNarmColor,
+                &tabview->bg1,
+                XmNbackground,
+                &tabview->bg2,
+                XmNhighlightColor,
+                &tabview->fg1,
+                XmNheight,
+                &h,
+                NULL);
+        tabview->height = h + 2; // border
+        
+        XtVaSetValues(tabview->tabbar, XmNbackground, tabview->bg1, NULL);
+    }
+    
+    tab.tab_button = button;
+    tab.child = child;
+    size_t newtab_index = cxListSize(tabview->tabs);
+    cxListAdd(tabview->tabs, &tab);
+    UiTab *newtab = cxListAt(tabview->tabs, newtab_index);
+    
+    XtAddCallback(
+            button,
+            XmNactivateCallback,
+            (XtCallbackProc)ui_tab_button_callback,
+            newtab);
+    
+    if(newtab_index == 0) {
+        ui_motif_tabview_change_tab(tabview, newtab);
+    } else {
+        XtVaSetValues(button, XmNbackground, tabview->bg1, NULL);
+    }
+}
+
+void ui_motif_tabview_remove(UiMotifTabView *tabview, int index) {
+    UiTab *tab = cxListAt(tabview->tabs, index);
+    if(tab) {
+        if(tab == tabview->current_tab) {
+            if(index > 0) {
+                ui_motif_tabview_select(tabview, index-1);
+            } else {
+                if(index < cxListSize(tabview->tabs)) {
+                    ui_motif_tabview_select(tabview, index+1);
+                } else {
+                    tabview->current_tab = NULL;
+                    tabview->current_index = -1;
+                }
+            }
+        }
+        XtDestroyWidget(tab->tab_button);
+        XtDestroyWidget(tab->child);
+        cxListRemove(tabview->tabs, index);
+    }
+}
+
+void ui_motif_tabview_change_tab(UiMotifTabView *tabview, UiTab *tab) {
+    if(tabview->current_tab) {
+        XtVaSetValues(tabview->current_tab->tab_button, XmNshadowThickness, 0, XmNbackground, tabview->bg1, NULL);
+        XtUnmanageChild(tabview->current_tab->child);
+    }
+    XtVaSetValues(tab->tab_button, XmNshadowThickness, 1, XmNbackground, tabview->bg2, NULL);
+    tabview->current_tab = tab;
+    tabview->current_index = (int)cxListFind(tabview->tabs, tab);;
+    XtManageChild(tab->child);
+}
+
+Widget ui_tabview_container_prepare(UiContainerPrivate *ctn, Arg *args, int *n) {
+    UiTabViewContainer *ct = (UiTabViewContainer*)ctn;
+    UiMotifTabView *tabview = ct->tabview;
+    int a = *n;
+    XtSetArg(args[a], XmNleftAttachment, XmATTACH_FORM); a++;
+    XtSetArg(args[a], XmNrightAttachment, XmATTACH_FORM); a++;
+    XtSetArg(args[a], XmNbottomAttachment, XmATTACH_FORM); a++;
+    XtSetArg(args[a], XmNtopAttachment, XmATTACH_WIDGET); a++;
+    XtSetArg(args[a], XmNtopWidget, tabview->tabbar); a++;
+    *n = a;
+    return tabview->form;
+}
+
+void ui_tabview_container_add(UiContainerPrivate *ctn, Widget widget) {
+    ui_reset_layout(ctn->layout);
+}
+
+
+
 /* -------------------- Container Helper Functions -------------------- */
 
 void ui_container_begin_close(UiObject *obj) {
--- a/ui/motif/container.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/motif/container.h	Tue Feb 25 21:11:00 2025 +0100
@@ -48,6 +48,12 @@
     layout.rowspan = args.rowspan
     
 typedef enum UiBoxOrientation UiBoxOrientation;
+
+enum UiContainerType {
+    UI_CONTAINER_GENERIC = 0,
+    UI_CONTAINER_TABVIEW
+};
+typedef enum UiContainerType UiContainerType;
     
 #define ui_reset_layout(layout) memset(&(layout), 0, sizeof(UiLayout))
 #define ui_lb2bool(b) ((b) == UI_LAYOUT_TRUE ? TRUE : FALSE)
@@ -79,11 +85,12 @@
 
 
 struct UiContainerPrivate {
-    UiContainerX container;
-    Widget       (*prepare)(UiContainerPrivate*, Arg *, int*);
-    void         (*add)(UiContainerPrivate*, Widget);
-    Widget       widget;
-    UiLayout     layout;
+    UiContainerX    container;
+    Widget          (*prepare)(UiContainerPrivate*, Arg *, int*);
+    void            (*add)(UiContainerPrivate*, Widget);
+    Widget          widget;
+    UiContainerType type;
+    UiLayout        layout;
 };
 
 typedef struct UiBoxContainer {
@@ -97,6 +104,49 @@
     Dimension y;
 } UiGridContainer;
 
+typedef struct UiTab {
+    Widget tab_button;
+    Widget child;
+} UiTab;
+
+typedef struct UiMotifTabView UiMotifTabView;
+struct UiMotifTabView {
+    UiObject *obj;
+    Widget form;
+    Widget tabbar;
+    Widget content;
+    Widget current;
+    UiTab *current_tab;
+    int current_index;
+    int height;
+    Pixel bg1;
+    Pixel bg2;
+    Pixel fg1;
+    GC gc;
+    int gc_initialized;
+    UiTabViewType tabview;
+    UiSubContainerType subcontainer;
+    CxList *tabs;
+    void (*select)(UiMotifTabView *tabview, int tab);
+    void (*add)(UiMotifTabView *tabview, int index, const char *name, Widget child);
+    void (*remove)(UiMotifTabView *tabview, int index);
+};
+
+typedef struct UiTabViewContainer {
+    UiContainerPrivate container;
+    UiMotifTabView *tabview;
+} UiTabViewContainer;
+
+void ui_motif_tabview_select(UiMotifTabView *tabview, int tab);
+void ui_motif_tabview_add_tab(UiMotifTabView *tabview, int index, const char *name, Widget child);
+void ui_motif_tabview_remove(UiMotifTabView *tabview, int index);
+void ui_motif_tabview_change_tab(UiMotifTabView *tabview, UiTab *tab);
+int64_t ui_tabview_get(UiInteger *i);
+void ui_tabview_set(UiInteger *i, int64_t value);
+
+Widget ui_tabview_container_prepare(UiContainerPrivate *ctn, Arg *args, int *n);
+void ui_tabview_container_add(UiContainerPrivate *ctn, Widget widget);
+
 UiContainerX* ui_box_container(UiObject *obj, Widget grid, UiBoxOrientation orientation);
 Widget ui_vbox_prepare(UiContainerPrivate *ctn, Arg *args, int *n);
 Widget ui_hbox_prepare(UiContainerPrivate *ctn, Arg *args, int *n);
--- a/ui/motif/list.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/motif/list.c	Tue Feb 25 21:11:00 2025 +0100
@@ -196,3 +196,78 @@
 void* ui_strmodel_getvalue(void *elm, int column) {
     return column == 0 ? elm : NULL;
 }
+
+/* ------------------------------- Drop Down ------------------------------- */
+
+static void ui_dropdown_selection(
+        Widget w,
+        UiListView *listview,
+        XmComboBoxCallbackStruct *cb)
+{
+    UiListSelection sel = { 0, NULL };
+    if(cb->item_position > 0) {
+        sel.count = 1;
+        sel.rows = malloc(sizeof(int));
+        sel.rows[0] = cb->item_position-1;
+    }
+    UiEvent event;
+    event.obj = listview->obj;
+    event.window = event.obj->window;
+    event.document = event.obj->ctx->document;
+    event.eventdata = &sel;
+    event.intval = 0;
+    if(listview->onactivate) {
+        listview->onactivate(&event, listview->onactivatedata);
+    }
+    if(listview->onselection) {
+        listview->onselection(&event, listview->onselectiondata);
+    }
+}
+
+UIWIDGET ui_combobox_create(UiObject* obj, UiListArgs args) {
+    Arg xargs[16];
+    int n = 0;
+    
+    UiContainerPrivate *ctn = ui_obj_container(obj);
+    UI_APPLY_LAYOUT(ctn->layout, args);
+    
+    char *name = args.name ? (char*)args.name : "dropdown";
+    Widget parent = ctn->prepare(ctn, xargs, &n);
+    Widget widget = XmCreateDropDownList(parent, name, xargs, n);
+    XtManageChild(widget);
+    
+    UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args.list, args.varname, UI_VAR_LIST);
+    
+    UiListView *listview = malloc(sizeof(UiListView));
+    memset(listview, 0, sizeof(UiListView));
+    listview->obj = obj;
+    listview->widget = widget;
+    listview->getvalue = args.getvalue ? args.getvalue : ui_strmodel_getvalue;
+    listview->var = var;
+    listview->onactivate = args.onactivate;
+    listview->onactivatedata = args.onactivatedata;
+    listview->onselection = args.onselection;
+    listview->onselectiondata = args.onselectiondata;
+    
+    if(var) {
+        UiList *list = var->value;
+        list->obj = listview;
+        list->update = ui_listview_update;
+        list->getselection = ui_listview_getselection;
+        list->setselection = ui_listview_setselection;
+        ui_listview_update(list, 0);
+    }
+    
+    XtAddCallback(
+                widget,
+                XmNdestroyCallback,
+                (XtCallbackProc)ui_listview_destroy,
+                listview);
+    XtAddCallback(
+                widget,
+                XmNselectionCallback,
+                (XtCallbackProc)ui_dropdown_selection,
+                listview);
+    
+    return widget;
+}
--- a/ui/motif/text.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/motif/text.c	Tue Feb 25 21:11:00 2025 +0100
@@ -1,7 +1,7 @@
 /*
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
  *
- * Copyright 2014 Olaf Wintermann. All rights reserved.
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are met:
@@ -36,6 +36,328 @@
 #include <cx/string.h>
 
 
+/* ------------------------------ Text Area ------------------------------ */
+
+UIWIDGET ui_textarea_create(UiObject *obj, UiTextAreaArgs args) {
+    Arg xargs[16];
+    int n = 0;
+    
+    XtSetArg(xargs[n], XmNeditMode, XmMULTI_LINE_EDIT); n++;
+    
+    UiContainerPrivate *ctn = ui_obj_container(obj);
+    UI_APPLY_LAYOUT(ctn->layout, args);
+    
+    Widget parent = ctn->prepare(ctn, xargs, &n);
+    char *name = args.name ? (char*)args.name : "textarea";
+    XtSetArg(xargs[n], XmNwidth, 100); n++;
+    Widget widget = XmCreateScrolledText(parent, name, xargs, n);
+    XtManageChild(widget);
+    
+    UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args.value, args.varname, UI_VAR_TEXT);
+    
+    UiTextArea *textarea = malloc(sizeof(UiTextArea));
+    memset(textarea, 0, sizeof(UiTextArea));
+    textarea->obj = obj;
+    textarea->var = var;
+    
+    if(var) {
+        UiText *value = var->value;
+        if(value->value.ptr) {
+            XmTextSetString(widget, value->value.ptr);
+            value->value.free(value->value.ptr);
+            value->value.ptr = NULL;
+        }
+        
+        value->set = ui_textarea_set;
+        value->get = ui_textarea_get;
+        value->getsubstr = ui_textarea_getsubstr;
+        value->insert = ui_textarea_insert;
+        value->setposition = ui_textarea_setposition;
+        value->position = ui_textarea_position;
+        value->selection = ui_textarea_selection;
+        value->length = ui_textarea_length;
+        value->value.ptr = NULL;
+        value->obj = widget;
+        
+        if(!value->undomgr) {
+            value->undomgr = ui_create_undomgr();
+        }
+        
+        XtAddCallback(
+                widget,
+                XmNmodifyVerifyCallback,
+                (XtCallbackProc)ui_text_modify_callback,
+                var);
+    }
+    
+    return widget;
+}
+
+char* ui_textarea_get(UiText *text) {
+    if(text->value.ptr) {
+        text->value.free(text->value.ptr);
+    }
+    char *str = XmTextGetString(text->obj);
+    text->value.ptr = str;
+    text->value.free = (ui_freefunc)XtFree;
+    return str;
+}
+
+void ui_textarea_set(UiText *text, const char *str) {
+    XmTextSetString(text->obj, (char*)str);
+    if(text->value.ptr) {
+        text->value.free(text->value.ptr);
+    }
+    text->value.ptr = NULL;
+}
+
+char* ui_textarea_getsubstr(UiText *text, int begin, int end) {
+    if(text->value.ptr) {
+        text->value.free(text->value.ptr);
+    }
+    int length = end - begin;
+    char *str = XtMalloc(length + 1);
+    XmTextGetSubstring(text->obj, begin, length, length + 1, str);
+    text->value.ptr = str;
+    text->value.free = (ui_freefunc)XtFree;
+    return str;
+}
+
+void ui_textarea_insert(UiText *text, int pos, char *str) {
+    text->value.ptr = NULL;
+    XmTextInsert(text->obj, pos, str);
+    if(text->value.ptr) {
+        text->value.free(text->value.ptr);
+    }
+}
+
+void ui_textarea_setposition(UiText *text, int pos) {
+    XmTextSetInsertionPosition(text->obj, pos);
+}
+
+int ui_textarea_position(UiText *text) {
+    long begin;
+    long end;
+    XmTextGetSelectionPosition(text->obj, &begin, &end);
+    text->pos = begin;
+    return text->pos;
+}
+
+void ui_textarea_selection(UiText *text, int *begin, int *end) {
+    XmTextGetSelectionPosition(text->obj, (long*)begin, (long*)end);
+}
+
+int ui_textarea_length(UiText *text) {
+    return (int)XmTextGetLastPosition(text->obj);
+}
+
+
+
+UiUndoMgr* ui_create_undomgr() {
+    UiUndoMgr *mgr = malloc(sizeof(UiUndoMgr));
+    mgr->begin = NULL;
+    mgr->end = NULL;
+    mgr->cur = NULL;
+    mgr->length = 0;
+    mgr->event = 1;
+    return mgr;
+}
+
+void ui_destroy_undomgr(UiUndoMgr *mgr) {
+    UiTextBufOp *op = mgr->begin;
+    while(op) {
+        UiTextBufOp *nextOp = op->next;
+        if(op->text) {
+            free(op->text);
+        }
+        free(op);
+        op = nextOp;
+    }
+    free(mgr);
+}
+
+void ui_text_selection_callback(
+        Widget widget,
+        UiTextArea *textarea,
+        XtPointer data)
+{
+    long left = 0;
+    long right = 0;
+    XmTextGetSelectionPosition(widget, &left, &right);
+    int sel = left < right ? 1 : 0;
+    if(sel != textarea->last_selection_state) {
+        if(sel) {
+            ui_set_group(textarea->obj->ctx, UI_GROUP_SELECTION);
+        } else {
+            ui_unset_group(textarea->obj->ctx, UI_GROUP_SELECTION);
+        }
+    }
+    textarea->last_selection_state = sel;
+}
+
+void ui_text_modify_callback(Widget widget, UiVar *var, XtPointer data) {
+    UiText *value = var->value;
+    if(!value->obj) {
+        // TODO: bug, fix
+        return;
+    }
+    if(!value->undomgr) {
+        value->undomgr = ui_create_undomgr();
+    }
+    
+    XmTextVerifyCallbackStruct *txv = (XmTextVerifyCallbackStruct*)data;
+    int type = txv->text->length > 0 ? UI_TEXTBUF_INSERT : UI_TEXTBUF_DELETE;
+    UiUndoMgr *mgr = value->undomgr;
+    if(!mgr->event) {
+        return;
+    }
+    
+    char *text = txv->text->ptr;
+    int length = txv->text->length;
+    
+    if(mgr->cur) {
+        UiTextBufOp *elm = mgr->cur->next;
+        if(elm) {
+            mgr->cur->next = NULL;
+            mgr->end = mgr->cur;
+            while(elm) {
+                elm->prev = NULL;   
+                UiTextBufOp *next = elm->next;
+                ui_free_textbuf_op(elm);
+                elm = next;
+            }
+        }
+        
+        UiTextBufOp *last_op = mgr->cur;
+        if(
+            last_op->type == UI_TEXTBUF_INSERT &&
+            ui_check_insertstr(last_op->text, last_op->len, text, length) == 0)
+        {
+            // append text to last op       
+            int ln = last_op->len;
+            char *newtext = malloc(ln + length + 1);
+            memcpy(newtext, last_op->text, ln);
+            memcpy(newtext+ln, text, length);
+            newtext[ln+length] = '\0';
+            
+            last_op->text = newtext;
+            last_op->len = ln + length;
+            last_op->end += length;
+            
+            return;
+        }
+    }
+    
+    char *str;
+    if(type == UI_TEXTBUF_INSERT) {
+        str = malloc(length + 1);
+        memcpy(str, text, length);
+        str[length] = 0;
+    } else {
+        length = txv->endPos - txv->startPos;
+        str = malloc(length + 1);
+        XmTextGetSubstring(value->obj, txv->startPos, length, length+1, str);
+    }
+    
+    UiTextBufOp *op = malloc(sizeof(UiTextBufOp));
+    op->prev = NULL;
+    op->next = NULL;
+    op->type = type;
+    op->start = txv->startPos;
+    op->end = txv->endPos + 1;
+    op->len = length;
+    op->text = str;
+    
+    cx_linked_list_add(
+            (void**)&mgr->begin,
+            (void**)&mgr->end,
+            offsetof(UiTextBufOp, prev),
+            offsetof(UiTextBufOp, next),
+            op);
+    
+    mgr->cur = op;
+}
+
+int ui_check_insertstr(char *oldstr, int oldlen, char *newstr, int newlen) {
+    // return 1 if oldstr + newstr are one word
+    
+    int has_space = 0;
+    for(int i=0;i<oldlen;i++) {
+        if(oldstr[i] < 33) {
+            has_space = 1;
+            break;
+        }
+    }
+    
+    for(int i=0;i<newlen;i++) {
+        if(has_space && newstr[i] > 32) {
+            return 1;
+        }
+    }
+    
+    return 0;
+}
+
+void ui_free_textbuf_op(UiTextBufOp *op) {
+    if(op->text) {
+        free(op->text);
+    }
+    free(op);
+}
+
+
+void ui_text_undo(UiText *value) {
+    UiUndoMgr *mgr = value->undomgr;
+    
+    if(mgr->cur) {
+        UiTextBufOp *op = mgr->cur;
+        mgr->event = 0;
+        switch(op->type) {
+            case UI_TEXTBUF_INSERT: {
+                XmTextReplace(value->obj, op->start, op->end, "");
+                break;
+            }
+            case UI_TEXTBUF_DELETE: {
+                XmTextInsert(value->obj, op->start, op->text);
+                break;
+            }
+        }
+        mgr->event = 1;
+        mgr->cur = mgr->cur->prev;
+    }
+}
+
+void ui_text_redo(UiText *value) {
+    UiUndoMgr *mgr = value->undomgr;
+    
+    UiTextBufOp *elm = NULL;
+    if(mgr->cur) {
+        if(mgr->cur->next) {
+            elm = mgr->cur->next;
+        }
+    } else if(mgr->begin) {
+        elm = mgr->begin;
+    }
+    
+    if(elm) {
+        UiTextBufOp *op = elm;
+        mgr->event = 0;
+        switch(op->type) {
+            case UI_TEXTBUF_INSERT: {
+                XmTextInsert(value->obj, op->start, op->text);
+                break;
+            }
+            case UI_TEXTBUF_DELETE: {
+                XmTextReplace(value->obj, op->start, op->end, "");
+                break;
+            }
+        }
+        mgr->event = 1;
+        mgr->cur = elm;
+    }
+}
+
+
 
 /* ------------------------------ Text Field ------------------------------ */
 
--- a/ui/motif/text.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/motif/text.h	Tue Feb 25 21:11:00 2025 +0100
@@ -1,7 +1,7 @@
 /*
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
  *
- * Copyright 2014 Olaf Wintermann. All rights reserved.
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are met:
@@ -36,6 +36,52 @@
 #ifdef	__cplusplus
 extern "C" {
 #endif
+    
+#define UI_TEXTBUF_INSERT 0
+#define UI_TEXTBUF_DELETE 1
+typedef struct UiTextBufOp UiTextBufOp;
+struct UiTextBufOp {
+    UiTextBufOp *prev;
+    UiTextBufOp *next;
+    int  type; // UI_TEXTBUF_INSERT, UI_TEXTBUF_DELETE
+    int  start;
+    int  end;
+    int  len;
+    char *text;
+};
+    
+typedef struct UiUndoMgr {
+    UiTextBufOp *begin;
+    UiTextBufOp *end;
+    UiTextBufOp *cur;
+    int         length;
+    int         event;
+} UiUndoMgr;
+
+typedef struct UiTextArea {
+    UiObject *obj;
+    UiVar *var;
+    int last_selection_state;
+} UiTextArea;
+
+char* ui_textarea_get(UiText *text);
+void ui_textarea_set(UiText *text, const char *str);
+char* ui_textarea_getsubstr(UiText *text, int begin, int end);
+void ui_textarea_insert(UiText *text, int pos, char *str);
+void ui_textarea_setposition(UiText *text, int pos);
+int ui_textarea_position(UiText *text);
+void ui_textarea_selection(UiText *text, int *begin, int *end);
+int ui_textarea_length(UiText *text);
+
+UiUndoMgr* ui_create_undomgr();
+void ui_destroy_undomgr(UiUndoMgr *mgr);
+void ui_text_selection_callback(
+        Widget widget,
+        UiTextArea *textarea,
+        XtPointer data);
+void ui_text_modify_callback(Widget widget, UiVar *var, XtPointer data);
+int ui_check_insertstr(char *oldstr, int oldlen, char *newstr, int newlen);
+void ui_free_textbuf_op(UiTextBufOp *op);
 
 char* ui_textfield_get(UiString *str);
 void ui_textfield_set(UiString *str, const char *value);
--- a/ui/motif/toolkit.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/motif/toolkit.c	Tue Feb 25 21:11:00 2025 +0100
@@ -78,6 +78,8 @@
         "*window_frame.shadowThickness: 1",
         "*togglebutton.shadowThickness: 1",
         "*togglebutton.highlightThickness: 2",
+        
+        "*ui_test.background: red",
 	NULL
 };
 
--- a/ui/motif/window.c	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/motif/window.c	Tue Feb 25 21:11:00 2025 +0100
@@ -57,7 +57,7 @@
 
 
 static UiObject* create_window(const char *title, void *window_data, Boolean simple) {
-    CxMempool *mp = cxBasicMempoolCreate(256);
+    CxMempool *mp = cxMempoolCreateSimple(256);
     const CxAllocator *a = mp->allocator;
     UiObject *obj = cxCalloc(a, 1, sizeof(UiObject));
     obj->ctx = uic_context(obj, mp);
--- a/ui/ui/button.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/button.h	Tue Feb 25 21:11:00 2025 +0100
@@ -41,6 +41,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -62,6 +63,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -94,6 +96,7 @@
 
 
 
+
 #ifdef	__cplusplus
 }
 #endif
--- a/ui/ui/container.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/container.h	Tue Feb 25 21:11:00 2025 +0100
@@ -57,12 +57,26 @@
     UI_HEADERBAR_ALTERNATIVE_BOX
 } UiHeaderbarAlternative;
 
+typedef struct UiWidgetArgs {
+    UiTri fill;
+    UiBool hexpand;
+    UiBool vexpand;
+    UiBool hfill;
+    UiBool vfill;
+    UiBool override_defaults;
+    int colspan;
+    int rowspan;
+    const char *name;
+    const char *style_class;
+} UiWidgetArgs;
+
 typedef struct UiContainerArgs {
     UiTri fill;
     UiBool hexpand;
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -72,6 +86,10 @@
     int spacing;
     int columnspacing;
     int rowspacing;
+    UiBool def_hfill;
+    UiBool def_vfill;
+    UiBool def_hexpand;
+    UiBool def_vexpand;
 } UiContainerArgs;
 
 typedef struct UiFrameArgs {
@@ -80,6 +98,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -102,6 +121,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -129,6 +149,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -148,12 +169,36 @@
     int spacing;
 } UiSidebarArgs;
 
+typedef struct UiSplitPaneArgs {
+    UiTri fill;
+    UiBool hexpand;
+    UiBool vexpand;
+    UiBool hfill;
+    UiBool vfill;
+    UiBool override_defaults;
+    int colspan;
+    int rowspan;
+    const char *name;
+    const char *style_class;
+
+    int margin;
+    int spacing;
+    int columnspacing;
+    int rowspacing;
+    
+    int initial_position;
+    UiInteger *value;
+    const char* varname;
+    int max_panes;
+} UiSplitPaneArgs;
+
 typedef struct UiItemListContainerArgs {
     UiTri fill;
     UiBool hexpand;
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -222,6 +267,13 @@
 #define ui_headerbar0(obj) for(ui_headerbar_create(obj, (UiHeaderbarArgs){ 0 });ui_container_finish(obj);ui_container_begin_close(obj))
 #define ui_sidebar0(obj) for(ui_sidebar_create(obj, (UiSidebarArgs){ 0 });ui_container_finish(obj);ui_container_begin_close(obj))
 
+#define ui_tabview_w(obj, w, ...) for(w = ui_tabview_create(obj, (UiTabViewArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
+
+#define ui_hsplitpane(obj, ...) for(ui_hsplitpane_create(obj, (UiSplitPaneArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
+#define ui_vsplitpane(obj, ...) for(ui_vsplitpane_create(obj, (UiSplitPaneArgs){ __VA_ARGS__ });ui_container_finish(obj);ui_container_begin_close(obj))
+#define ui_hsplitpane0(obj) for(ui_hsplitpane_create(obj, (UiSplitPaneArgs){ 0 });ui_container_finish(obj);ui_container_begin_close(obj))
+#define ui_vsplitpane0(obj) for(ui_vsplitpane_create(obj, (UiSplitPaneArgs){ 0 });ui_container_finish(obj);ui_container_begin_close(obj))
+
 #define ui_tab(obj, label) for(ui_tab_create(obj, label);ui_container_finish(obj);ui_container_begin_close(obj))
 
 #define ui_headerbar_start(obj) for(ui_headerbar_start_create(obj);ui_container_finish(obj);ui_container_begin_close(obj))
@@ -255,8 +307,8 @@
 
 UIEXPORT UIWIDGET ui_itemlist_create(UiObject *obj, UiItemListContainerArgs args);
 
-UIEXPORT UIWIDGET ui_hsplitpane(UiObject *obj, int max); // TODO
-UIEXPORT UIWIDGET ui_vsplitpane(UiObject *obj, int max); // TODO
+UIEXPORT UIWIDGET ui_hsplitpane_create(UiObject *obj, UiSplitPaneArgs args);
+UIEXPORT UIWIDGET ui_vsplitpane_create(UiObject *obj, UiSplitPaneArgs args);
 
 
 // box container layout functions
@@ -266,6 +318,7 @@
 UIEXPORT void ui_layout_vexpand(UiObject *obj, UiBool expand);
 UIEXPORT void ui_layout_hfill(UiObject *obj, UiBool fill);
 UIEXPORT void ui_layout_vfill(UiObject *obj, UiBool fill);
+UIEXPORT void ui_layout_override_defaults(UiObject *obj, UiBool d);
 UIEXPORT void ui_layout_width(UiObject *obj, int width);
 UIEXPORT void ui_layout_height(UiObject* obj, int width);
 UIEXPORT void ui_layout_colspan(UiObject *obj, int cols);
@@ -277,6 +330,18 @@
 UIEXPORT UiObject* ui_document_tab(UiTabbedPane *view);
 
 
+#ifdef UI_GTK
+typedef UIWIDGET (*ui_createwidget_func)(UiObject *obj, UiWidgetArgs args, void *userdata);
+#elif defined(UI_MOTIF)
+typedef UIWIDGET (*ui_createwidget_func)(UiObject *obj, UiWidgetArgs args, void *userdata, Widget parent, Arg *a, int n);
+#elif defined(UI_COCOA)
+typedef UIWIDGET (*ui_createwidget_func)(UiObject *obj, UiWidgetArgs args, void *userdata);
+#endif
+UIEXPORT UIWIDGET ui_customwidget_create(UiObject *obj, ui_createwidget_func create_widget, void *userdata, UiWidgetArgs args);
+
+#define ui_customwidget(obj, create_widget, userdata, ...) ui_customwidget_create(obj, create_widget, userdata, (UiWidgetArgs) { __VA_ARGS__ })
+
+
 /* used for macro */
 UIEXPORT void ui_container_begin_close(UiObject *obj);
 UIEXPORT int ui_container_finish(UiObject *obj);
@@ -287,6 +352,7 @@
     if(args.vexpand) ui_layout_vexpand(obj, 1); \
     if(args.hfill) ui_layout_hfill(obj, 1); \
     if(args.vfill) ui_layout_vfill(obj, 1); \
+    if(args.override_defaults) ui_layout_override_defaults(obj, 1); \
     if(args.colspan > 0) ui_layout_colspan(obj, args.colspan); \
     if(args.rowspan > 0) ui_layout_rowspan(obj, args.rowspan); \
     /*force caller to add ';'*/(void)0
--- a/ui/ui/display.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/display.h	Tue Feb 25 21:11:00 2025 +0100
@@ -63,6 +63,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -81,6 +82,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     int width;
@@ -99,6 +101,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
--- a/ui/ui/entry.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/entry.h	Tue Feb 25 21:11:00 2025 +0100
@@ -42,6 +42,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
--- a/ui/ui/image.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/image.h	Tue Feb 25 21:11:00 2025 +0100
@@ -43,6 +43,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
--- a/ui/ui/range.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/range.h	Tue Feb 25 21:11:00 2025 +0100
@@ -35,8 +35,8 @@
 extern "C" {
 #endif
 
-UIWIDGET ui_hscrollbar(UiObject *obj, UiRange *range, ui_callback f, void *userdata);
-UIWIDGET ui_vscrollbar(UiObject *obj, UiRange *range, ui_callback f, void *userdata);
+UIWIDGET ui_hscrollbar(UiObject *obj, UiRange *range, ui_callback f, void *userdata); // TODO
+UIWIDGET ui_vscrollbar(UiObject *obj, UiRange *range, ui_callback f, void *userdata); // TODO
 
 
 
--- a/ui/ui/text.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/text.h	Tue Feb 25 21:11:00 2025 +0100
@@ -41,6 +41,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     int width;
@@ -61,6 +62,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     int width;
@@ -94,6 +96,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
--- a/ui/ui/toolkit.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/toolkit.h	Tue Feb 25 21:11:00 2025 +0100
@@ -42,6 +42,7 @@
 typedef void* UIMENU;   // NSMenu*
 
 #elif UI_GTK2 || UI_GTK3 || UI_GTK4
+#define UI_GTK
 
 #include <gtk/gtk.h>
 #define UIWIDGET GtkWidget*
@@ -533,6 +534,7 @@
 
 
 UIEXPORT UiList* ui_list_new(UiContext *ctx, char *name);
+UIEXPORT void ui_list_free(UiList *list);
 UIEXPORT void* ui_list_first(UiList *list);
 UIEXPORT void* ui_list_next(UiList *list);
 UIEXPORT void* ui_list_get(UiList *list, int i);
--- a/ui/ui/tree.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/tree.h	Tue Feb 25 21:11:00 2025 +0100
@@ -109,6 +109,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -144,6 +145,15 @@
     void *userdata;
 };
 
+typedef struct UiSubListEventData {
+    UiList *list;
+    int    sublist_index;
+    int    row_index;
+    void   *row_data;
+    void   *sublist_userdata;
+    void   *event_data;
+} UiSubListEventData;
+
 /*
  * list item members must be filled by the sublist getvalue func
  * all members must be allocated (by malloc, strdup, ...) the pointer
@@ -164,6 +174,7 @@
     UiBool vexpand;
     UiBool hfill;
     UiBool vfill;
+    UiBool override_defaults;
     int colspan;
     int rowspan;
     const char *name;
@@ -172,11 +183,14 @@
     const int *groups;
     
     /*
-     * list of sublists
+     * static list of sublists
      * a sublist must have a varname or a value
      * 
      * the last entry in the list must contain all NULL values or numsublists
      * must contain the number of sublists
+     * 
+     * sublists can be NULL, in which case sublists are dynamically loaded
+     * from dynamic_sublist/varname
      */
     UiSubList *sublists;
     /*
@@ -187,6 +201,17 @@
     size_t numsublists;
     
     /*
+     * list value, that contains UiSubList* elements
+     */
+    UiList *dynamic_sublist;
+    
+    /*
+     * load sublists dynamically from a variable with the specified name
+     */
+    const char *varname;
+    
+    
+    /*
      * callback for each list item, that should fill all necessary
      * UiSubListItem fields
      */
--- a/ui/ui/ui.h	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/ui/ui.h	Tue Feb 25 21:11:00 2025 +0100
@@ -47,5 +47,7 @@
 #include "dnd.h"
 #include "icons.h"
 
+#include "webview.h"
+
 #endif	/* UI_H */
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/ui/webview.h	Tue Feb 25 21:11:00 2025 +0100
@@ -0,0 +1,90 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef UI_WEBVIEW_H
+#define UI_WEBVIEW_H
+
+#include "toolkit.h"
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+    
+#define UI_WEBVIEW_OBJECT_TYPE "webview"
+    
+typedef struct UiWebviewArgs {
+    UiTri fill;
+    UiBool hexpand;
+    UiBool vexpand;
+    UiBool hfill;
+    UiBool vfill;
+    UiBool override_defaults;
+    int colspan;
+    int rowspan;
+    const char *name;
+    const char *style_class;
+    
+    UiGeneric *value;
+    const char *varname;
+    
+    const int* groups;
+} UiWebviewArgs;
+
+#define ui_webview(obj, ...) ui_webview_create(obj, (UiWebviewArgs){ __VA_ARGS__ } )
+
+UIWIDGET ui_webview_create(UiObject *obj, UiWebviewArgs args);
+
+void ui_webview_load_url(UiGeneric *g, const char *url);
+
+void ui_webview_load_content(
+        UiGeneric *g,
+        const char *uri,
+        const char *content,
+        size_t contentlength,
+        const char *mimetype,
+        const char *encoding);
+
+
+void ui_webview_reload(UiGeneric *g);
+UiBool ui_webview_can_go_back(UiGeneric *g);
+UiBool ui_webview_can_go_forward(UiGeneric *g);
+void ui_webview_go_back(UiGeneric *g);
+void ui_webview_go_forward(UiGeneric *g);
+const char * ui_webview_get_uri(UiGeneric *g);
+void ui_webview_enable_javascript(UiGeneric *g, UiBool enable);
+void ui_webview_set_zoom(UiGeneric *g, double zoom);
+double ui_webview_get_zoom(UiGeneric *g);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UI_WEBVIEW_H */
+
--- a/ui/winui/window.cpp	Mon Jan 06 22:22:55 2025 +0100
+++ b/ui/winui/window.cpp	Tue Feb 25 21:11:00 2025 +0100
@@ -142,7 +142,7 @@
 }
 
 UIEXPORT UiObject* ui_simple_window(const char *title, void *window_data) {
-	CxMempool* mp = cxBasicMempoolCreate(256);
+	CxMempool* mp = cxMempoolCreateSimple(256);
 	UiObject* obj = (UiObject*)cxCalloc(mp->allocator, 1, sizeof(UiObject));
 
 	obj->ctx = uic_context(obj, mp);
@@ -223,7 +223,7 @@
 		return NULL;
 	}
 	
-	CxMempool* mp = cxBasicMempoolCreate(256);
+	CxMempool* mp = cxMempoolCreateSimple(256);
 	UiObject* obj = (UiObject*)cxCalloc(mp->allocator, 1, sizeof(UiObject));
 	
 	obj->ctx = uic_context(obj, mp);

mercurial