stabilizes mempool implementation

Fri, 23 May 2025 13:36:11 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 23 May 2025 13:36:11 +0200
changeset 1328
2cf66dee40b8
parent 1327
ed75dc1db503
child 1329
343eac5ac824

stabilizes mempool implementation

resolves #676
fixes #677
fixes #678

CHANGELOG file | annotate | diff | comparison | revisions
docs/Writerside/topics/about.md file | annotate | diff | comparison | revisions
src/cx/mempool.h file | annotate | diff | comparison | revisions
src/mempool.c file | annotate | diff | comparison | revisions
tests/test_mempool.c file | annotate | diff | comparison | revisions
tests/ucxtest.c file | annotate | diff | comparison | revisions
--- a/CHANGELOG	Fri May 23 12:44:24 2025 +0200
+++ b/CHANGELOG	Fri May 23 13:36:11 2025 +0200
@@ -21,6 +21,8 @@
  * changes all cxListIterator() without index to also accept NULL as list argument
  * changes insert_element member function of CxList to accept NULL source and return a pointer to the inserted element
  * fixes critical memory overflow in the stack-based array reallocator (this unfortunately breaks the function signature)
+ * fixes mempool implementation not supporting NULL as argument for realloc
+ * fixes mempool implementation not supporting zero as size for realloc
  * fixes that starting an iteration in a non-root node incorrectly continues iteration with the siblings of that node
  * fixes unnecessary allocations in cx_strcat() family of functions
  * fixes errno value after failing cxBufferSeek() to be consistently EINVAL
--- a/docs/Writerside/topics/about.md	Fri May 23 12:44:24 2025 +0200
+++ b/docs/Writerside/topics/about.md	Fri May 23 13:36:11 2025 +0200
@@ -48,6 +48,8 @@
 * changes all cxListIterator() without index to also accept NULL as list argument
 * changes insert_element member function of CxList to accept NULL source and return a pointer to the inserted element
 * fixes critical memory overflow in the stack-based array reallocator (this unfortunately breaks the function signature)
+* fixes mempool implementation not supporting NULL as argument for realloc
+* fixes mempool implementation not supporting zero as size for realloc
 * fixes that starting an iteration in a non-root node incorrectly continues iteration with the siblings of that node
 * fixes unnecessary allocations in cx_strcat() family of functions
 * fixes errno value after failing cxBufferSeek() to be consistently EINVAL
--- a/src/cx/mempool.h	Fri May 23 12:44:24 2025 +0200
+++ b/src/cx/mempool.h	Fri May 23 13:36:11 2025 +0200
@@ -43,6 +43,7 @@
 extern "C" {
 #endif
 
+/** A memory block in a simple memory pool. */
 struct cx_mempool_memory_s {
     /** The destructor. */
     cx_destructor_func destructor;
@@ -50,6 +51,7 @@
     char c[];
 };
 
+/** A memory block in an advanced memory pool. */
 struct cx_mempool_memory2_s {
     /** The destructor. */
     cx_destructor_func2 destructor;
@@ -59,6 +61,7 @@
     char c[];
 };
 
+/** Represents memory that is not allocated by, but registered with a pool. */
 struct cx_mempool_foreign_memory_s {
     /** The foreign memory. */
     void* mem;
@@ -170,6 +173,36 @@
 CxMempool *cxMempoolCreate(size_t capacity, enum cx_mempool_type type);
 
 /**
+ * Creates a basic array-based memory pool.
+ *
+ * Convenience macro to create a memory pool of type #CX_MEMPOOL_TYPE_SIMPLE.
+ *
+ * @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 cxMempoolCreateSimple(capacity) cxMempoolCreate(capacity, CX_MEMPOOL_TYPE_SIMPLE)
+
+/**
+ * Creates a basic array-based memory pool.
+ *
+ * Convenience macro to create a memory pool of type #CX_MEMPOOL_TYPE_ADVANCED.
+ *
+ * @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 cxMempoolCreateAdvanced(capacity) cxMempoolCreate(capacity, CX_MEMPOOL_TYPE_ADVANCED)
+
+/**
+ * Creates a basic array-based memory pool.
+ *
+ * Convenience macro to create a memory pool of type #CX_MEMPOOL_TYPE_PURE.
+ *
+ * @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 cxMempoolCreatePure(capacity) cxMempoolCreate(capacity, CX_MEMPOOL_TYPE_PURE)
+
+/**
  * Sets the global destructor for all memory blocks within the specified pool.
  *
  * @param pool the memory pool
@@ -191,14 +224,6 @@
 void cxMempoolGlobalDestructor2(CxMempool *pool, cx_destructor_func2 fnc, void *data);
 
 /**
- * Creates a basic array-based memory pool.
- *
- * @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 cxMempoolCreateSimple(capacity) cxMempoolCreate(capacity, CX_MEMPOOL_TYPE_SIMPLE)
-
-/**
  * Sets the destructor function for a specific allocated memory object.
  *
  * If the type of memory pool is not #CX_MEMPOOL_TYPE_SIMPLE, the behavior is undefined.
--- a/src/mempool.c	Fri May 23 12:44:24 2025 +0200
+++ b/src/mempool.c	Fri May 23 13:36:11 2025 +0200
@@ -39,11 +39,12 @@
     size_t newcap = pool->capacity >= 1000 ?
         pool->capacity + 1000 : pool->capacity * 2;
     size_t newmsize;
+    // LCOV_EXCL_START
     if (pool->capacity > newcap
             || cx_szmul(newcap, sizeof(void*), &newmsize)) {
         errno = EOVERFLOW;
         return 1;
-    }
+    } // LCOV_EXCL_STOP
     void **newdata = cxReallocDefault(pool->data, newmsize);
     if (newdata == NULL) return 1;
     pool->data = newdata;
@@ -59,11 +60,12 @@
     // we do not expect so many registrations
     size_t newcap = pool->registered_capacity + 8;
     size_t newmsize;
+    // LCOV_EXCL_START
     if (pool->registered_capacity > newcap || cx_szmul(newcap,
             sizeof(struct cx_mempool_foreign_memory_s), &newmsize)) {
         errno = EOVERFLOW;
         return 1;
-    }
+    } // LCOV_EXCL_STOP
     void *newdata = cxReallocDefault(pool->registered, newmsize);
     if (newdata == NULL) return 1;
     pool->registered = newdata;
@@ -78,7 +80,7 @@
     struct cx_mempool_s *pool = p;
 
     if (cx_mempool_ensure_capacity(pool, pool->size + 1)) {
-        return NULL;
+        return NULL; // LCOV_EXCL_LINE
     }
 
     struct cx_mempool_memory_s *mem =
@@ -107,33 +109,6 @@
     return ptr;
 }
 
-static void *cx_mempool_realloc_simple(
-        void *p,
-        void *ptr,
-        size_t n
-) {
-    struct cx_mempool_s *pool = p;
-
-    const unsigned overhead = sizeof(struct cx_mempool_memory_s);
-    struct cx_mempool_memory_s *mem =
-        (void *) (((char *) ptr) - overhead);
-    struct cx_mempool_memory_s *newm =
-        cxReallocDefault(mem, n + overhead);
-
-    if (newm == NULL) return NULL;
-    if (mem != newm) {
-        for (size_t i = 0; i < pool->size; i++) {
-            if (pool->data[i] == mem) {
-                pool->data[i] = newm;
-                return ((char*)newm) + overhead;
-            }
-        }
-        abort(); // LCOV_EXCL_LINE
-    } else {
-        return ptr;
-    }
-}
-
 static void cx_mempool_free_simple(
         void *p,
         void *ptr
@@ -168,6 +143,41 @@
     abort(); // LCOV_EXCL_LINE
 }
 
+static void *cx_mempool_realloc_simple(
+        void *p,
+        void *ptr,
+        size_t n
+) {
+    if (ptr == NULL) {
+        return cx_mempool_malloc_simple(p, n);
+    }
+    if (n == 0) {
+        cx_mempool_free_simple(p, ptr);
+        return NULL;
+    }
+    struct cx_mempool_s *pool = p;
+
+    const unsigned overhead = sizeof(struct cx_mempool_memory_s);
+    struct cx_mempool_memory_s *mem =
+        (void *) (((char *) ptr) - overhead);
+    struct cx_mempool_memory_s *newm =
+        cxReallocDefault(mem, n + overhead);
+
+    if (newm == NULL) return NULL;
+    if (mem != newm) {
+        for (size_t i = 0; i < pool->size; i++) {
+            if (pool->data[i] == mem) {
+                pool->data[i] = newm;
+                return ((char*)newm) + overhead;
+            }
+        }
+        abort(); // LCOV_EXCL_LINE
+    } else {
+        // unfortunately glibc() realloc seems to always move
+        return ptr;  // LCOV_EXCL_LINE
+    }
+}
+
 static void cx_mempool_free_all_simple(const struct cx_mempool_s *pool) {
     const bool has_destr = pool->destr;
     const bool has_destr2 = pool->destr2;
@@ -200,11 +210,11 @@
     struct cx_mempool_s *pool = p;
 
     if (cx_mempool_ensure_capacity(pool, pool->size + 1)) {
-        return NULL;
+        return NULL;  // LCOV_EXCL_LINE
     }
 
     struct cx_mempool_memory2_s *mem =
-        cxMallocDefault(sizeof(struct cx_mempool_memory_s) + n);
+        cxMallocDefault(sizeof(struct cx_mempool_memory2_s) + n);
     if (mem == NULL) return NULL;
     mem->destructor = NULL;
     mem->data = NULL;
@@ -230,33 +240,6 @@
     return ptr;
 }
 
-static void *cx_mempool_realloc_advanced(
-        void *p,
-        void *ptr,
-        size_t n
-) {
-    struct cx_mempool_s *pool = p;
-
-    const unsigned overhead = sizeof(struct cx_mempool_memory2_s);
-    struct cx_mempool_memory2_s *mem =
-        (void *) (((char *) ptr) - overhead);
-    struct cx_mempool_memory2_s *newm =
-        cxReallocDefault(mem, n + overhead);
-
-    if (newm == NULL) return NULL;
-    if (mem != newm) {
-        for (size_t i = 0; i < pool->size; i++) {
-            if (pool->data[i] == mem) {
-                pool->data[i] = newm;
-                return ((char*)newm) + overhead;
-            }
-        }
-        abort(); // LCOV_EXCL_LINE
-    } else {
-        return ptr;
-    }
-}
-
 static void cx_mempool_free_advanced(
         void *p,
         void *ptr
@@ -291,6 +274,41 @@
     abort(); // LCOV_EXCL_LINE
 }
 
+static void *cx_mempool_realloc_advanced(
+        void *p,
+        void *ptr,
+        size_t n
+) {
+    if (ptr == NULL) {
+        return cx_mempool_malloc_advanced(p, n);
+    }
+    if (n == 0) {
+        cx_mempool_free_advanced(p, ptr);
+        return NULL;
+    }
+    struct cx_mempool_s *pool = p;
+
+    const unsigned overhead = sizeof(struct cx_mempool_memory2_s);
+    struct cx_mempool_memory2_s *mem =
+        (void *) (((char *) ptr) - overhead);
+    struct cx_mempool_memory2_s *newm =
+        cxReallocDefault(mem, n + overhead);
+
+    if (newm == NULL) return NULL;
+    if (mem != newm) {
+        for (size_t i = 0; i < pool->size; i++) {
+            if (pool->data[i] == mem) {
+                pool->data[i] = newm;
+                return ((char*)newm) + overhead;
+            }
+        }
+        abort(); // LCOV_EXCL_LINE
+    } else {
+        // unfortunately glibc() realloc seems to always move
+        return ptr;  // LCOV_EXCL_LINE
+    }
+}
+
 static void cx_mempool_free_all_advanced(const struct cx_mempool_s *pool) {
     const bool has_destr = pool->destr;
     const bool has_destr2 = pool->destr2;
@@ -324,7 +342,7 @@
     struct cx_mempool_s *pool = p;
 
     if (cx_mempool_ensure_capacity(pool, pool->size + 1)) {
-        return NULL;
+        return NULL; // LCOV_EXCL_LINE
     }
 
     void *mem = cxMallocDefault(n);
@@ -351,27 +369,6 @@
     return ptr;
 }
 
-static void *cx_mempool_realloc_pure(
-        void *p,
-        void *ptr,
-        size_t n
-) {
-    struct cx_mempool_s *pool = p;
-    void *newm = cxReallocDefault(ptr, n);
-    if (newm == NULL) return NULL;
-    if (ptr != newm) {
-        for (size_t i = 0; i < pool->size; i++) {
-            if (pool->data[i] == ptr) {
-                pool->data[i] = newm;
-                return newm;
-            }
-        }
-        abort(); // LCOV_EXCL_LINE
-    } else {
-        return ptr;
-    }
-}
-
 static void cx_mempool_free_pure(
         void *p,
         void *ptr
@@ -400,6 +397,35 @@
     abort(); // LCOV_EXCL_LINE
 }
 
+static void *cx_mempool_realloc_pure(
+        void *p,
+        void *ptr,
+        size_t n
+) {
+    if (ptr == NULL) {
+        return cx_mempool_malloc_pure(p, n);
+    }
+    if (n == 0) {
+        cx_mempool_free_pure(p, ptr);
+        return NULL;
+    }
+    struct cx_mempool_s *pool = p;
+    void *newm = cxReallocDefault(ptr, n);
+    if (newm == NULL) return NULL;
+    if (ptr != newm) {
+        for (size_t i = 0; i < pool->size; i++) {
+            if (pool->data[i] == ptr) {
+                pool->data[i] = newm;
+                return newm;
+            }
+        }
+        abort(); // LCOV_EXCL_LINE
+    } else {
+        // unfortunately glibc() realloc seems to always move
+        return ptr;  // LCOV_EXCL_LINE
+    }
+}
+
 static void cx_mempool_free_all_pure(const struct cx_mempool_s *pool) {
     const bool has_destr = pool->destr;
     const bool has_destr2 = pool->destr2;
@@ -526,9 +552,10 @@
     if (capacity == 0) capacity = 16;
     size_t poolsize;
     if (cx_szmul(capacity, sizeof(void*), &poolsize)) {
+        // LCOV_EXCL_START
         errno = EOVERFLOW;
         return NULL;
-    }
+    }  // LCOV_EXCL_STOP
 
     CxAllocator *provided_allocator = cxMallocDefault(sizeof(CxAllocator));
     if (provided_allocator == NULL) { // LCOV_EXCL_START
@@ -536,10 +563,10 @@
     } // LCOV_EXCL_STOP
 
     CxMempool *pool = cxCallocDefault(1, sizeof(CxMempool));
-    if (pool == NULL) {
+    if (pool == NULL) {  // LCOV_EXCL_START
         cxFreeDefault(provided_allocator);
         return NULL;
-    }
+    }  // LCOV_EXCL_STOP
 
     provided_allocator->data = pool;
     pool->allocator = provided_allocator;
--- a/tests/test_mempool.c	Fri May 23 12:44:24 2025 +0200
+++ b/tests/test_mempool.c	Fri May 23 13:36:11 2025 +0200
@@ -31,6 +31,19 @@
 
 #include "cx/mempool.h"
 
+#include <errno.h>
+
+static unsigned test_mempool_destructor_called;
+
+static void test_mempool_destructor(cx_attr_unused void *mem) {
+    test_mempool_destructor_called++;
+}
+
+static void test_mempool_destructor2(void *data, cx_attr_unused void *mem) {
+    int *ctr = data;
+    *ctr = *ctr + 1;
+}
+
 CX_TEST(test_mempool_create) {
     CxMempool *pool = cxMempoolCreateSimple(16);
     CX_TEST_DO {
@@ -51,21 +64,69 @@
     cxMempoolFree(pool);
 }
 
+static CX_TEST_SUBROUTINE(test_mempool_malloc_verify, CxMempool *pool) {
+    CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
+    CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
+    CX_TEST_ASSERT(pool->size == 2);
+    CX_TEST_ASSERT(pool->capacity == 4);
+    CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
+    CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
+    CX_TEST_ASSERT(pool->size == 4);
+    CX_TEST_ASSERT(pool->capacity == 4);
+    CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
+    int *i = cxMalloc(pool->allocator, sizeof(int));
+    CX_TEST_ASSERT(i != NULL);
+    *i = 4083914; // let asan / valgrind check
+    CX_TEST_ASSERT(pool->size == 6);
+    CX_TEST_ASSERT(pool->capacity >= 6);
+}
+
+CX_TEST(test_mempool_malloc0) {
+    CxMempool *pool = cxMempoolCreatePure(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_malloc_verify, pool);
+    }
+    cxMempoolFree(pool);
+}
+
 CX_TEST(test_mempool_malloc) {
     CxMempool *pool = cxMempoolCreateSimple(4);
     CX_TEST_DO {
-        CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
-        CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
-        CX_TEST_ASSERT(pool->size == 2);
-        CX_TEST_ASSERT(pool->capacity == 4);
-        CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
-        CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
-        CX_TEST_ASSERT(pool->size == 4);
-        CX_TEST_ASSERT(pool->capacity == 4);
-        CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
-        CX_TEST_ASSERT(cxMalloc(pool->allocator, sizeof(int)) != NULL);
-        CX_TEST_ASSERT(pool->size == 6);
-        CX_TEST_ASSERT(pool->capacity >= 6);
+        CX_TEST_CALL_SUBROUTINE(test_mempool_malloc_verify, pool);
+    }
+    cxMempoolFree(pool);
+}
+
+CX_TEST(test_mempool_malloc2) {
+    CxMempool *pool = cxMempoolCreateAdvanced(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_malloc_verify, pool);
+    }
+    cxMempoolFree(pool);
+}
+
+static CX_TEST_SUBROUTINE(test_mempool_calloc_verify, CxMempool *pool) {
+    int *test = cxCalloc(pool->allocator, 2, sizeof(int));
+    CX_TEST_ASSERT(test != NULL);
+    CX_TEST_ASSERT(test[0] == 0);
+    CX_TEST_ASSERT(test[1] == 0);
+#if __GNUC__ > 11
+// we want to explicitly test the overflow
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Walloc-size-larger-than="
+#endif
+    errno = 0;
+    CX_TEST_ASSERT(NULL == cxCalloc(pool->allocator, SIZE_MAX / 2, sizeof(int)));
+    CX_TEST_ASSERT(errno == EOVERFLOW);
+#if __GNUC__ > 11
+#pragma GCC diagnostic pop
+#endif
+}
+
+CX_TEST(test_mempool_calloc0) {
+    CxMempool *pool = cxMempoolCreatePure(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_calloc_verify, pool);
     }
     cxMempoolFree(pool);
 }
@@ -73,66 +134,121 @@
 CX_TEST(test_mempool_calloc) {
     CxMempool *pool = cxMempoolCreateSimple(4);
     CX_TEST_DO {
-        int *test = cxCalloc(pool->allocator, 2, sizeof(int));
-        CX_TEST_ASSERT(test != NULL);
-        CX_TEST_ASSERT(test[0] == 0);
-        CX_TEST_ASSERT(test[1] == 0);
+        CX_TEST_CALL_SUBROUTINE(test_mempool_calloc_verify, pool);
+    }
+    cxMempoolFree(pool);
+}
+
+CX_TEST(test_mempool_calloc2) {
+    CxMempool *pool = cxMempoolCreateAdvanced(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_calloc_verify, pool);
     }
     cxMempoolFree(pool);
 }
 
-static unsigned test_mempool_destructor_called;
+static CX_TEST_SUBROUTINE(test_mempool_realloc_verify, CxMempool *pool, enum cx_mempool_type type) {
+    // use realloc with NULL which shall behave as a malloc
+    int *data = cxRealloc(pool->allocator, NULL, 2*sizeof(int));
+    if (type == CX_MEMPOOL_TYPE_SIMPLE) {
+        cxMempoolSetDestructor(data, test_mempool_destructor);
+    } else if (type == CX_MEMPOOL_TYPE_ADVANCED) {
+        cxMempoolSetDestructor2(data, test_mempool_destructor2, &test_mempool_destructor_called);
+    }
+    *data = 13;
+
+    // shrink to actual sizeof(int)
+    data = cxRealloc(pool->allocator, data, sizeof(int));
+    CX_TEST_ASSERT(*data == 13);
+
+    // realloc with the same size (should not do anything)
+    data = cxRealloc(pool->allocator, data, sizeof(int));
+    CX_TEST_ASSERT(*data == 13);
 
-static void test_mempool_destructor(cx_attr_unused void *mem) {
-    test_mempool_destructor_called++;
+    // now try hard to trigger a memmove
+    int *rdata = data;
+    unsigned n = 1;
+    while (rdata == data) {
+        n <<= 1;
+        // eventually the memory should be moved elsewhere
+        CX_TEST_ASSERTM(n < 65536, "Reallocation attempt failed - test not executable");
+        rdata = cxRealloc(pool->allocator, data, n * sizeof(intptr_t));
+    }
+
+    CX_TEST_ASSERT(*rdata == 13);
+    // test if destructor is still intact
+    if (type != CX_MEMPOOL_TYPE_PURE) {
+        test_mempool_destructor_called = 0;
+        cxFree(pool->allocator, rdata);
+        CX_TEST_ASSERT(test_mempool_destructor_called == 1);
+    }
+}
+
+CX_TEST(test_mempool_realloc0) {
+    CxMempool *pool = cxMempoolCreatePure(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_realloc_verify, pool, CX_MEMPOOL_TYPE_PURE);
+    }
+    cxMempoolFree(pool);
 }
 
 CX_TEST(test_mempool_realloc) {
     CxMempool *pool = cxMempoolCreateSimple(4);
-    cxMempoolGlobalDestructor(pool, test_mempool_destructor);
     CX_TEST_DO {
-        CX_TEST_ASSERT(pool->destr == test_mempool_destructor);
-        int *data = cxMalloc(pool->allocator, sizeof(int));
-        *data = 13;
+        CX_TEST_CALL_SUBROUTINE(test_mempool_realloc_verify, pool, CX_MEMPOOL_TYPE_SIMPLE);
+    }
+    cxMempoolFree(pool);
+}
 
-        int *rdata = data;
-        unsigned n = 1;
-        while (rdata == data) {
-            n <<= 1;
-            // eventually the memory should be moved elsewhere
-            CX_TEST_ASSERTM(n < 65536, "Reallocation attempt failed - test not executable");
-            rdata = cxRealloc(pool->allocator, data, n * sizeof(intptr_t));
-        }
-
-        CX_TEST_ASSERT(*rdata == 13);
-        // test if destructor is still intact
-        test_mempool_destructor_called = 0;
-        cxFree(pool->allocator, rdata);
-        CX_TEST_ASSERT(test_mempool_destructor_called == 1);
+CX_TEST(test_mempool_realloc2) {
+    CxMempool *pool = cxMempoolCreateAdvanced(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_realloc_verify, pool, CX_MEMPOOL_TYPE_ADVANCED);
     }
     cxMempoolFree(pool);
 }
 
+static CX_TEST_SUBROUTINE(test_mempool_free_verify, CxMempool *pool) {
+    void *mem1, *mem2;
+    mem1 = cxMalloc(pool->allocator, 16);
+    cxFree(pool->allocator, mem1);
+    CX_TEST_ASSERT(pool->size == 0);
+
+    mem1 = cxMalloc(pool->allocator, 16);
+    mem1 = cxMalloc(pool->allocator, 16);
+    mem1 = cxMalloc(pool->allocator, 16);
+    mem2 = cxMalloc(pool->allocator, 16);
+    mem2 = cxMalloc(pool->allocator, 16);
+
+    CX_TEST_ASSERT(pool->size == 5);
+    // a realloc with size zero shall behave like a free
+    void *freed = cxRealloc(pool->allocator, mem1, 0);
+    CX_TEST_ASSERT(freed == NULL);
+    CX_TEST_ASSERT(pool->size == 4);
+    cxFree(pool->allocator, mem2);
+    CX_TEST_ASSERT(pool->size == 3);
+}
+
+CX_TEST(test_mempool_free0) {
+    CxMempool *pool = cxMempoolCreatePure(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_free_verify, pool);
+    }
+    cxMempoolFree(pool);
+}
 
 CX_TEST(test_mempool_free) {
     CxMempool *pool = cxMempoolCreateSimple(4);
-    void *mem1, *mem2;
     CX_TEST_DO {
-        mem1 = cxMalloc(pool->allocator, 16);
-        cxFree(pool->allocator, mem1);
-        CX_TEST_ASSERT(pool->size == 0);
+        CX_TEST_CALL_SUBROUTINE(test_mempool_free_verify, pool);
+    }
+    cxMempoolFree(pool);
+}
 
-        mem1 = cxMalloc(pool->allocator, 16);
-        mem1 = cxMalloc(pool->allocator, 16);
-        mem1 = cxMalloc(pool->allocator, 16);
-        mem2 = cxMalloc(pool->allocator, 16);
-        mem2 = cxMalloc(pool->allocator, 16);
-
-        CX_TEST_ASSERT(pool->size == 5);
-        cxFree(pool->allocator, mem1);
-        CX_TEST_ASSERT(pool->size == 4);
-        cxFree(pool->allocator, mem2);
-        CX_TEST_ASSERT(pool->size == 3);
+CX_TEST(test_mempool_free2) {
+    CxMempool *pool = cxMempoolCreateAdvanced(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_free_verify, pool);
     }
     cxMempoolFree(pool);
 }
@@ -154,14 +270,124 @@
     }
 }
 
+CX_TEST(test_mempool_destroy2) {
+    CxMempool *pool = cxMempoolCreateAdvanced(4);
+    CX_TEST_DO {
+        int *data = cxMalloc(pool->allocator, sizeof(int));
+        int ctr = 0;
+        *data = 47;
+        cxMempoolSetDestructor2(data, test_mempool_destructor2, &ctr);
+        CX_TEST_ASSERT(*data == 47);
+        cxFree(pool->allocator, data);
+        CX_TEST_ASSERT(ctr == 1);
+        data = cxMalloc(pool->allocator, sizeof(int));
+        cxMempoolSetDestructor2(data, test_mempool_destructor2, &ctr);
+        cxMempoolFree(pool);
+        CX_TEST_ASSERT(ctr == 2);
+    }
+}
+
+CX_TEST(test_mempool_remove_destructor) {
+    CxMempool *pool = cxMempoolCreateSimple(4);
+    CX_TEST_DO {
+        int *data = cxMalloc(pool->allocator, sizeof(int));
+        *data = 13;
+        cxMempoolSetDestructor(data, test_mempool_destructor);
+        CX_TEST_ASSERT(*data == 13);
+        cxMempoolRemoveDestructor(data);
+        CX_TEST_ASSERT(*data == 13);
+        test_mempool_destructor_called = 0;
+        cxFree(pool->allocator, data);
+        CX_TEST_ASSERT(test_mempool_destructor_called == 0);
+        data = cxMalloc(pool->allocator, sizeof(int));
+        *data = 99;
+        cxMempoolSetDestructor(data, test_mempool_destructor);
+        cxMempoolRemoveDestructor(data);
+        CX_TEST_ASSERT(*data == 99);
+        cxMempoolFree(pool);
+        CX_TEST_ASSERT(test_mempool_destructor_called == 0);
+    }
+}
+
+CX_TEST(test_mempool_remove_destructor2) {
+    CxMempool *pool = cxMempoolCreateAdvanced(4);
+    CX_TEST_DO {
+        int *data = cxMalloc(pool->allocator, sizeof(int));
+        int ctr = 0;
+        *data = 47;
+        cxMempoolSetDestructor2(data, test_mempool_destructor2, &ctr);
+        CX_TEST_ASSERT(*data == 47);
+        cxMempoolRemoveDestructor2(data);
+        CX_TEST_ASSERT(*data == 47);
+        cxFree(pool->allocator, data);
+        CX_TEST_ASSERT(ctr == 0);
+        data = cxMalloc(pool->allocator, sizeof(int));
+        *data = 99;
+        cxMempoolSetDestructor2(data, test_mempool_destructor2, &ctr);
+        cxMempoolRemoveDestructor2(data);
+        CX_TEST_ASSERT(*data == 99);
+        cxMempoolFree(pool);
+        CX_TEST_ASSERT(ctr == 0);
+    }
+}
+
+static CX_TEST_SUBROUTINE(test_mempool_global_destructors_verify, CxMempool *pool) {
+    int *data = cxMalloc(pool->allocator, sizeof(int));
+    int ctr = 0;
+    cxMempoolGlobalDestructor(pool, test_mempool_destructor);
+    cxMempoolGlobalDestructor2(pool, test_mempool_destructor2, &ctr);
+    test_mempool_destructor_called = 0;
+    cxFree(pool->allocator, data);
+    CX_TEST_ASSERT(ctr == 1);
+    CX_TEST_ASSERT(test_mempool_destructor_called == 1);
+    data = cxMalloc(pool->allocator, sizeof(int));
+    cxMempoolFree(pool);
+    CX_TEST_ASSERT(ctr == 2);
+    CX_TEST_ASSERT(test_mempool_destructor_called == 2);
+}
+
+CX_TEST(test_mempool_global_destructors0) {
+    CxMempool *pool = cxMempoolCreatePure(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_global_destructors_verify, pool);
+    }
+}
+
+CX_TEST(test_mempool_global_destructors) {
+    CxMempool *pool = cxMempoolCreateSimple(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_global_destructors_verify, pool);
+    }
+}
+
+CX_TEST(test_mempool_global_destructors2) {
+    CxMempool *pool = cxMempoolCreateAdvanced(4);
+    CX_TEST_DO {
+        CX_TEST_CALL_SUBROUTINE(test_mempool_global_destructors_verify, pool);
+    }
+}
+
 CX_TEST(test_mempool_register) {
+    CxMempool *pool = cxMempoolCreateAdvanced(4);
+    CX_TEST_DO {
+        int *data = cxMalloc(pool->allocator, sizeof(int));
+        test_mempool_destructor_called = 0;
+        cxMempoolSetDestructor2(data, test_mempool_destructor2, &test_mempool_destructor_called);
+        int donotfree = 0;
+        cxMempoolRegister(pool, &donotfree, test_mempool_destructor);
+        cxMempoolFree(pool);
+        CX_TEST_ASSERT(test_mempool_destructor_called == 2);
+    }
+}
+
+CX_TEST(test_mempool_register2) {
     CxMempool *pool = cxMempoolCreateSimple(4);
     CX_TEST_DO {
         int *data = cxMalloc(pool->allocator, sizeof(int));
         test_mempool_destructor_called = 0;
         cxMempoolSetDestructor(data, test_mempool_destructor);
         int donotfree = 0;
-        cxMempoolRegister(pool, &donotfree, test_mempool_destructor);
+        cxMempoolRegister2(pool, &donotfree, test_mempool_destructor2, &test_mempool_destructor_called);
         cxMempoolFree(pool);
         CX_TEST_ASSERT(test_mempool_destructor_called == 2);
     }
@@ -222,50 +448,75 @@
     CxMempool *src = cxMempoolCreateSimple(4);
     CxMempool *dest = cxMempoolCreateSimple(4);
     CX_TEST_DO {
+        int *a = cxMalloc(src->allocator, sizeof(int));
         int *b = cxMalloc(src->allocator, sizeof(int));
-        b = cxMalloc(src->allocator, sizeof(int));
         int *c = malloc(sizeof(int));
         cxMempoolRegister(src, c, free);
+        int *d = malloc(sizeof(int));
+        cxMempoolRegister(src, d, free);
         CX_TEST_ASSERT(src->size == 2);
-        CX_TEST_ASSERT(src->registered_size == 1);
+        CX_TEST_ASSERT(src->registered_size == 2);
 
-        int result = cxMempoolTransferObject(src, dest, b);
+        int result = cxMempoolTransferObject(src, dest, a);
         CX_TEST_ASSERT(result == 0);
         CX_TEST_ASSERT(src->size == 1);
+        CX_TEST_ASSERT(src->registered_size == 2);
         CX_TEST_ASSERT(dest->size == 1);
-        result = cxMempoolTransferObject(src, dest, b);
+        CX_TEST_ASSERT(dest->registered_size == 0);
+        result = cxMempoolTransferObject(src, dest, a);
+        CX_TEST_ASSERT(result != 0);
+        CX_TEST_ASSERT(src->size == 1);
+        CX_TEST_ASSERT(src->registered_size == 2);
+        CX_TEST_ASSERT(dest->size == 1);
+        CX_TEST_ASSERT(dest->registered_size == 0);
+
+        // can also transfer foreign memory this way
+        result = cxMempoolTransferObject(src, dest, c);
+        CX_TEST_ASSERT(result == 0);
+        CX_TEST_ASSERT(src->size == 1);
+        CX_TEST_ASSERT(src->registered_size == 1);
+        CX_TEST_ASSERT(dest->size == 1);
+        CX_TEST_ASSERT(dest->registered_size == 1);
+
+        // src==dest is an error
+        result = cxMempoolTransferObject(dest, dest, b);
         CX_TEST_ASSERT(result != 0);
         CX_TEST_ASSERT(src->size == 1);
         CX_TEST_ASSERT(dest->size == 1);
 
-        // can also transfer foreign memory this way
-        CX_TEST_ASSERT(src->registered_size == 1);
-        CX_TEST_ASSERT(dest->registered_size == 0);
-        result = cxMempoolTransferObject(src, dest, c);
-        CX_TEST_ASSERT(result == 0);
-        CX_TEST_ASSERT(src->registered_size == 0);
-        CX_TEST_ASSERT(dest->registered_size == 1);
-
-        result = cxMempoolTransferObject(dest, dest, b);
-        CX_TEST_ASSERT(result != 0);
-        CX_TEST_ASSERT(src->size == 1);
-        CX_TEST_ASSERT(dest->size == 1);
+        // check that we don't die when we free memory that's still in the source pool
+        cxFree(src->allocator, b);
     }
     cxMempoolFree(src);
     cxMempoolFree(dest);
-    // let valgrind check that everything worked
+    // let valgrind check that everything else worked
 }
 
 CxTestSuite *cx_test_suite_mempool(void) {
     CxTestSuite *suite = cx_test_suite_new("mempool");
 
     cx_test_register(suite, test_mempool_create);
+    cx_test_register(suite, test_mempool_malloc0);
+    cx_test_register(suite, test_mempool_calloc0);
+    cx_test_register(suite, test_mempool_realloc0);
+    cx_test_register(suite, test_mempool_free0);
     cx_test_register(suite, test_mempool_malloc);
     cx_test_register(suite, test_mempool_calloc);
     cx_test_register(suite, test_mempool_realloc);
     cx_test_register(suite, test_mempool_free);
     cx_test_register(suite, test_mempool_destroy);
+    cx_test_register(suite, test_mempool_malloc2);
+    cx_test_register(suite, test_mempool_calloc2);
+    cx_test_register(suite, test_mempool_realloc2);
+    cx_test_register(suite, test_mempool_free2);
+    cx_test_register(suite, test_mempool_destroy2);
+    cx_test_register(suite, test_mempool_remove_destructor);
+    cx_test_register(suite, test_mempool_remove_destructor2);
+    cx_test_register(suite, test_mempool_global_destructors0);
+    cx_test_register(suite, test_mempool_global_destructors);
+    cx_test_register(suite, test_mempool_global_destructors2);
     cx_test_register(suite, test_mempool_register);
+    cx_test_register(suite, test_mempool_register2);
     cx_test_register(suite, test_mempool_transfer);
     cx_test_register(suite, test_mempool_transfer_object);
 
--- a/tests/ucxtest.c	Fri May 23 12:44:24 2025 +0200
+++ b/tests/ucxtest.c	Fri May 23 13:36:11 2025 +0200
@@ -59,7 +59,7 @@
     for (size_t i = 0; i < cx_nmemb(test_suites) ; i++) {run_tests(test_suites[i]);} (void)0
 #define free_test_suites for (size_t i = 0 ; i < cx_nmemb(test_suites) ; i++) {cx_test_suite_free(test_suites[i]);} (void)0
 
-static int verify_testing_allocator() {
+static int verify_testing_allocator(void) {
     printf("Verify Testing Allocator\n");
     CxTestSuite *suite_ta = cx_test_suite_testing_allocator();
     int result = suite_ta->failure > 0;

mercurial