add copy-on-write feature to UCX buffer - fixes #531

Wed, 18 Dec 2024 15:35:42 +0100

author
Mike Becker <universe@uap-core.de>
date
Wed, 18 Dec 2024 15:35:42 +0100
changeset 1024
8f99f6c28bd3
parent 1017
b0098854071f
child 1025
439407fc29e1

add copy-on-write feature to UCX buffer - fixes #531

src/buffer.c file | annotate | diff | comparison | revisions
src/cx/buffer.h file | annotate | diff | comparison | revisions
tests/test_buffer.c file | annotate | diff | comparison | revisions
--- a/src/buffer.c	Sun Dec 15 15:23:29 2024 +0100
+++ b/src/buffer.c	Wed Dec 18 15:35:42 2024 +0100
@@ -31,6 +31,19 @@
 #include <stdio.h>
 #include <string.h>
 
+static int buffer_copy_on_write(CxBuffer* buffer, size_t newcap) {
+    if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) return 0;
+    if (newcap == 0) newcap = buffer->capacity;
+    void *newspace = cxMalloc(buffer->allocator, newcap);
+    if (NULL == newspace) return -1;
+    memcpy(newspace, buffer->space, buffer->size);
+    buffer->space = newspace;
+    buffer->capacity = newcap;
+    buffer->flags &= ~CX_BUFFER_COPY_ON_WRITE;
+    buffer->flags |= CX_BUFFER_FREE_CONTENTS;
+    return 0;
+}
+
 int cxBufferInit(
         CxBuffer *buffer,
         void *space,
@@ -46,7 +59,7 @@
     if (!space) {
         buffer->bytes = cxMalloc(allocator, capacity);
         if (buffer->bytes == NULL) {
-            return 1;
+            return -1;
         }
         buffer->flags |= CX_BUFFER_FREE_CONTENTS;
     } else {
@@ -66,7 +79,7 @@
 }
 
 void cxBufferDestroy(CxBuffer *buffer) {
-    if ((buffer->flags & CX_BUFFER_FREE_CONTENTS) == CX_BUFFER_FREE_CONTENTS) {
+    if (buffer->flags & CX_BUFFER_FREE_CONTENTS) {
         cxFree(buffer->allocator, buffer->bytes);
     }
 }
@@ -133,7 +146,9 @@
 }
 
 void cxBufferClear(CxBuffer *buffer) {
-    memset(buffer->bytes, 0, buffer->size);
+    if (0 == (buffer->flags & CX_BUFFER_COPY_ON_WRITE)) {
+        memset(buffer->bytes, 0, buffer->size);
+    }
     buffer->size = 0;
     buffer->pos = 0;
 }
@@ -155,7 +170,9 @@
         return 0;
     }
 
-    if (cxReallocate(buffer->allocator,
+    if (buffer->flags & CX_BUFFER_COPY_ON_WRITE) {
+        return buffer_copy_on_write(buffer, newcap);
+    } else if (cxReallocate(buffer->allocator,
                      (void **) &buffer->bytes, newcap) == 0) {
         buffer->capacity = newcap;
         return 0;
@@ -207,6 +224,7 @@
 ) {
     // optimize for easy case
     if (size == 1 && (buffer->capacity - buffer->pos) >= nitems) {
+        if (buffer_copy_on_write(buffer, 0)) return 0;
         memcpy(buffer->bytes + buffer->pos, ptr, nitems);
         buffer->pos += nitems;
         if (buffer->pos > buffer->size) {
@@ -227,7 +245,7 @@
 
     bool perform_flush = false;
     if (required > buffer->capacity) {
-        if ((buffer->flags & CX_BUFFER_AUTO_EXTEND) == CX_BUFFER_AUTO_EXTEND && required) {
+        if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
             if (buffer->flush_blkmax > 0 && required > buffer->flush_threshold) {
                 perform_flush = true;
             } else {
@@ -293,6 +311,7 @@
             return cxBufferWrite(ptr, size, nitems, buffer);
         }
     } else {
+        if (buffer_copy_on_write(buffer, 0)) return 0;
         memcpy(buffer->bytes + buffer->pos, ptr, len);
         buffer->pos += len;
         if (buffer->pos > buffer->size) {
@@ -323,7 +342,7 @@
         buffer->size--;
         return 0;
     } else {
-        return 1;
+        return -1;
     }
 }
 
@@ -376,6 +395,7 @@
     if (shift >= buffer->size) {
         buffer->pos = buffer->size = 0;
     } else {
+        if (buffer_copy_on_write(buffer, 0)) return -1;
         memmove(buffer->bytes, buffer->bytes + shift, buffer->size - shift);
         buffer->size -= shift;
 
@@ -397,9 +417,9 @@
 
     // auto extend buffer, if required and enabled
     if (buffer->capacity < req_capacity) {
-        if ((buffer->flags & CX_BUFFER_AUTO_EXTEND) == CX_BUFFER_AUTO_EXTEND) {
+        if (buffer->flags & CX_BUFFER_AUTO_EXTEND) {
             if (cxBufferMinimumCapacity(buffer, req_capacity)) {
-                return 1;
+                return -1;
             }
             movebytes = buffer->size;
         } else {
@@ -409,8 +429,11 @@
         movebytes = buffer->size;
     }
 
-    memmove(buffer->bytes + shift, buffer->bytes, movebytes);
-    buffer->size = shift + movebytes;
+    if (movebytes > 0) {
+        if (buffer_copy_on_write(buffer, 0)) return -1;
+        memmove(buffer->bytes + shift, buffer->bytes, movebytes);
+        buffer->size = shift + movebytes;
+    }
 
     buffer->pos += shift;
     if (buffer->pos > buffer->size) {
--- a/src/cx/buffer.h	Sun Dec 15 15:23:29 2024 +0100
+++ b/src/cx/buffer.h	Wed Dec 18 15:35:42 2024 +0100
@@ -60,14 +60,25 @@
 
 /**
  * If this flag is enabled, the buffer will automatically free its contents when destroyed.
+ *
+ * Do NOT set this flag together with #CX_BUFFER_COPY_ON_WRITE. It will be automatically
+ * set when the copy-on-write operations is performed.
  */
 #define CX_BUFFER_FREE_CONTENTS 0x01
 
 /**
- * If this flag is enabled, the buffer will automatically extends its capacity.
+ * If this flag is enabled, the buffer will automatically extend its capacity.
  */
 #define CX_BUFFER_AUTO_EXTEND 0x02
 
+/**
+ * If this flag is enabled, the buffer will allocate new memory when written to.
+ *
+ * The current contents of the buffer will be copied to the new memory and the flag
+ * will be cleared while the #CX_BUFFER_FREE_CONTENTS flag will be set automatically.
+ */
+#define CX_BUFFER_COPY_ON_WRITE 0x04
+
 /** Structure for the UCX buffer data. */
 typedef struct {
     /** A pointer to the buffer contents. */
@@ -128,6 +139,7 @@
      * @see #CX_BUFFER_DEFAULT
      * @see #CX_BUFFER_FREE_CONTENTS
      * @see #CX_BUFFER_AUTO_EXTEND
+     * @see #CX_BUFFER_COPY_ON_WRITE
      */
     int flags;
 } cx_buffer_s;
@@ -140,9 +152,15 @@
 /**
  * Initializes a fresh buffer.
  *
+ * You may also provide a read-only \p space, in which case
+ * you will need to cast the pointer, and you should set the
+ * #CX_BUFFER_COPY_ON_WRITE flag.
+ *
  * \note You may provide \c NULL as argument for \p space.
  * Then this function will allocate the space and enforce
- * the #CX_BUFFER_FREE_CONTENTS flag.
+ * the #CX_BUFFER_FREE_CONTENTS flag. In that case, specifying
+ * copy-on-write should be avoided, because the allocated
+ * space will be leaking after the copy-on-write operation.
  *
  * @param buffer the buffer to initialize
  * @param space pointer to the memory area, or \c NULL to allocate
@@ -192,6 +210,10 @@
 /**
  * Allocates and initializes a fresh buffer.
  *
+ * You may also provide a read-only \p space, in which case
+ * you will need to cast the pointer, and you should set the
+ * #CX_BUFFER_COPY_ON_WRITE flag.
+ *
  * \note You may provide \c NULL as argument for \p space.
  * Then this function will allocate the space and enforce
  * the #CX_BUFFER_FREE_CONTENTS flag.
@@ -246,7 +268,7 @@
  *
  * @param buffer the buffer
  * @param shift the shift offset (negative means left shift)
- * @return 0 on success, non-zero if a required auto-extension fails
+ * @return 0 on success, non-zero if a required auto-extension or copy-on-write fails
  */
 cx_attr_nonnull
 int cxBufferShift(
@@ -260,7 +282,7 @@
  *
  * @param buffer the buffer
  * @param shift the shift offset
- * @return 0 on success, non-zero if a required auto-extension fails
+ * @return 0 on success, non-zero if a required auto-extension or copy-on-write fails
  * @see cxBufferShift()
  */
 cx_attr_nonnull
@@ -273,12 +295,9 @@
  * Shifts the buffer to the left.
  * See cxBufferShift() for details.
  *
- * \note Since a left shift cannot fail due to memory allocation problems, this
- * function always returns zero.
- *
  * @param buffer the buffer
  * @param shift the positive shift offset
- * @return always zero
+ * @return usually zero, except the buffer uses copy-on-write and the allocation fails
  * @see cxBufferShift()
  */
 cx_attr_nonnull
@@ -320,6 +339,9 @@
  * The data is deleted by zeroing it with a call to memset().
  * If you do not need that, you can use the faster cxBufferReset().
  *
+ * \note If the #CX_BUFFER_COPY_ON_WRITE flag is set, this function
+ * will not erase the data and behave exactly as cxBufferReset().
+ *
  * @param buffer the buffer to be cleared
  * @see cxBufferReset()
  */
--- a/tests/test_buffer.c	Sun Dec 15 15:23:29 2024 +0100
+++ b/tests/test_buffer.c	Wed Dec 18 15:35:42 2024 +0100
@@ -215,6 +215,22 @@
     cxBufferDestroy(&buf);
 }
 
+CX_TEST(test_buffer_clear_copy_on_write) {
+    char space[16];
+    strcpy(space, "clear test");
+    CxBuffer buf;
+    cxBufferInit(&buf, space, 16, cxDefaultAllocator, CX_BUFFER_COPY_ON_WRITE);
+    CX_TEST_DO {
+        buf.size = 5;
+        buf.pos = 3;
+        cxBufferClear(&buf);
+        CX_TEST_ASSERT(0 == memcmp(space, "clear test", 10));
+        CX_TEST_ASSERT(buf.size == 0);
+        CX_TEST_ASSERT(buf.pos == 0);
+    }
+    cxBufferDestroy(&buf);
+}
+
 CX_TEST(test_buffer_reset) {
     char space[16];
     strcpy(space, "reset test");
@@ -506,6 +522,24 @@
     }
 }
 
+CX_TEST(test_buffer_shift_left_copy_on_write) {
+    TEST_BUFFER_SHIFT_SETUP(buf);
+    buf.flags |= CX_BUFFER_COPY_ON_WRITE;
+    char *original = buf.space;
+    CX_TEST_DO {
+        int ret = cxBufferShiftLeft(&buf, 2);
+        CX_TEST_ASSERT(0 == (buf.flags & CX_BUFFER_COPY_ON_WRITE));
+        CX_TEST_ASSERT(0 != (buf.flags & CX_BUFFER_FREE_CONTENTS));
+        CX_TEST_ASSERT(ret == 0);
+        CX_TEST_ASSERT(buf.pos == 2);
+        CX_TEST_ASSERT(buf.size == 2);
+        CX_TEST_ASSERT(memcmp(original, "test____XXXXXXXX", 16) == 0);
+        CX_TEST_ASSERT(memcmp(buf.space, "st", 2) == 0);
+        cxFree(buf.allocator, original);
+        TEST_BUFFER_SHIFT_TEARDOWN(buf);
+    }
+}
+
 CX_TEST(test_buffer_shift_right_zero) {
     TEST_BUFFER_SHIFT_SETUP(buf);
     CX_TEST_DO {
@@ -584,6 +618,24 @@
     }
 }
 
+CX_TEST(test_buffer_shift_right_copy_on_write) {
+    TEST_BUFFER_SHIFT_SETUP(buf);
+    buf.flags |= CX_BUFFER_COPY_ON_WRITE;
+    char *original = buf.space;
+    CX_TEST_DO {
+        int ret = cxBufferShiftRight(&buf, 3);
+        CX_TEST_ASSERT(0 == (buf.flags & CX_BUFFER_COPY_ON_WRITE));
+        CX_TEST_ASSERT(0 != (buf.flags & CX_BUFFER_FREE_CONTENTS));
+        CX_TEST_ASSERT(ret == 0);
+        CX_TEST_ASSERT(buf.pos == 7);
+        CX_TEST_ASSERT(buf.size == 7);
+        CX_TEST_ASSERT(memcmp(original, "test____XXXXXXXX", 16) == 0);
+        CX_TEST_ASSERT(memcmp(buf.space, "testest", 7) == 0);
+        cxFree(buf.allocator, original);
+        TEST_BUFFER_SHIFT_TEARDOWN(buf);
+    }
+}
+
 static size_t mock_write_limited_rate(
         const void *ptr,
         size_t size,
@@ -657,6 +709,28 @@
     cxBufferDestroy(&buf);
 }
 
+CX_TEST(test_buffer_write_copy_on_write) {
+    CxBuffer buf;
+    char original[16] = "preparedXXXXXXX\0";
+    cxBufferInit(&buf, original, 16, cxDefaultAllocator, CX_BUFFER_COPY_ON_WRITE);
+    buf.capacity = 8;
+    buf.size = 8;
+    buf.pos = 0;
+    const char *data = "testing";
+    CX_TEST_DO {
+        size_t written = cxBufferWrite(data, 1, 7, &buf);
+        CX_TEST_ASSERT(written == 7);
+        CX_TEST_ASSERT(buf.size == 8);
+        CX_TEST_ASSERT(buf.pos == 7);
+        CX_TEST_ASSERT(buf.capacity == 8);
+        CX_TEST_ASSERT(0 == memcmp(buf.space, "testingd", 8));
+        CX_TEST_ASSERT(0 == memcmp(original, "preparedXXXXXXX\0", 16));
+        CX_TEST_ASSERT(0 == (buf.flags & CX_BUFFER_COPY_ON_WRITE));
+        CX_TEST_ASSERT(0 != (buf.flags & CX_BUFFER_FREE_CONTENTS));
+    }
+    cxBufferDestroy(&buf);
+}
+
 CX_TEST(test_buffer_write_multibyte_fit) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -770,6 +844,40 @@
     cxBufferDestroy(&buf);
 }
 
+CX_TEST(test_buffer_put_copy_on_write) {
+    CxBuffer buf;
+    char original[16] = "preparedXXXXXXX\0";
+    cxBufferInit(&buf, original, 16, cxDefaultAllocator, CX_BUFFER_COPY_ON_WRITE);
+    buf.capacity = 8;
+    buf.size = 8;
+    buf.pos = 8;
+    CX_TEST_DO {
+        int c = cxBufferPut(&buf, 0x200 | 'a');
+        CX_TEST_ASSERT(c == EOF);
+        CX_TEST_ASSERT(buf.size == 8);
+        CX_TEST_ASSERT(buf.pos == 8);
+        CX_TEST_ASSERT(buf.capacity == 8);
+        CX_TEST_ASSERT(0 == memcmp(buf.space, "prepared", 8));
+        // discarded, no write happend!
+        CX_TEST_ASSERT(original == buf.space);
+        CX_TEST_ASSERT(0 != (buf.flags & CX_BUFFER_COPY_ON_WRITE));
+        CX_TEST_ASSERT(0 == (buf.flags & CX_BUFFER_FREE_CONTENTS));
+        // now actually write somewhere
+        buf.pos = 2;
+        c = cxBufferPut(&buf, 0x200 | 'a');
+        CX_TEST_ASSERT(c == 'a');
+        CX_TEST_ASSERT(buf.size == 8);
+        CX_TEST_ASSERT(buf.pos == 3);
+        CX_TEST_ASSERT(buf.capacity == 8);
+        CX_TEST_ASSERT(0 == memcmp(buf.space, "prapared", 8));
+        CX_TEST_ASSERT(original != buf.space);
+        CX_TEST_ASSERT(0 == memcmp(original, "preparedXXXXXXX\0", 16));
+        CX_TEST_ASSERT(0 == (buf.flags & CX_BUFFER_COPY_ON_WRITE));
+        CX_TEST_ASSERT(0 != (buf.flags & CX_BUFFER_FREE_CONTENTS));
+    }
+    cxBufferDestroy(&buf);
+}
+
 CX_TEST(test_buffer_put_string_fit) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -825,6 +933,30 @@
     cxBufferDestroy(&buf);
 }
 
+CX_TEST(test_buffer_put_string_extend_copy_on_write) {
+    CxBuffer buf;
+    char original[16] = "preparedXXXXXXX\0";
+    cxBufferInit(&buf, original, 16, cxDefaultAllocator, CX_BUFFER_COPY_ON_WRITE);
+    buf.capacity = 8;
+    buf.size = 8;
+    buf.pos = 4;
+    buf.flags |= CX_BUFFER_AUTO_EXTEND;
+    const char *data = "testing";
+    CX_TEST_DO {
+        size_t written = cxBufferPutString(&buf, data);
+        CX_TEST_ASSERT(written == 7);
+        CX_TEST_ASSERT(buf.size == 11);
+        CX_TEST_ASSERT(buf.pos == 11);
+        CX_TEST_ASSERT(buf.capacity >= 11);
+        CX_TEST_ASSERT(0 == memcmp(buf.space, "preptesting", 11));
+        CX_TEST_ASSERT(original != buf.space);
+        CX_TEST_ASSERT(0 == memcmp(original, "preparedXXXXXXX\0", 16));
+        CX_TEST_ASSERT(0 == (buf.flags & CX_BUFFER_COPY_ON_WRITE));
+        CX_TEST_ASSERT(0 != (buf.flags & CX_BUFFER_FREE_CONTENTS));
+    }
+    cxBufferDestroy(&buf);
+}
+
 CX_TEST(test_buffer_terminate) {
     CxBuffer buf;
     cxBufferInit(&buf, NULL, 16, cxDefaultAllocator, CX_BUFFER_DEFAULT);
@@ -1113,6 +1245,7 @@
     cx_test_register(suite, test_buffer_minimum_capacity_sufficient);
     cx_test_register(suite, test_buffer_minimum_capacity_extend);
     cx_test_register(suite, test_buffer_clear);
+    cx_test_register(suite, test_buffer_clear_copy_on_write);
     cx_test_register(suite, test_buffer_reset);
     cx_test_register(suite, test_buffer_seek_set_zero);
     cx_test_register(suite, test_buffer_seek_set_valid);
@@ -1134,24 +1267,29 @@
     cx_test_register(suite, test_buffer_shift_left_overshift);
     cx_test_register(suite, test_buffer_shift_left_overshift_pos_only);
     cx_test_register(suite, test_buffer_shift_left_offset_interface);
+    cx_test_register(suite, test_buffer_shift_left_copy_on_write);
     cx_test_register(suite, test_buffer_shift_right_zero);
     cx_test_register(suite, test_buffer_shift_right_zero_offset_interface);
     cx_test_register(suite, test_buffer_shift_right_standard);
     cx_test_register(suite, test_buffer_shift_right_overshift_discard);
     cx_test_register(suite, test_buffer_shift_right_overshift_extend);
     cx_test_register(suite, test_buffer_shift_right_offset_interface);
+    cx_test_register(suite, test_buffer_shift_right_copy_on_write);
     cx_test_register(suite, test_buffer_write_size_one_fit);
     cx_test_register(suite, test_buffer_write_size_one_discard);
     cx_test_register(suite, test_buffer_write_size_one_extend);
     cx_test_register(suite, test_buffer_write_multibyte_fit);
     cx_test_register(suite, test_buffer_write_multibyte_discard);
     cx_test_register(suite, test_buffer_write_multibyte_extend);
+    cx_test_register(suite, test_buffer_write_copy_on_write);
     cx_test_register(suite, test_buffer_put_fit);
     cx_test_register(suite, test_buffer_put_discard);
     cx_test_register(suite, test_buffer_put_extend);
+    cx_test_register(suite, test_buffer_put_copy_on_write);
     cx_test_register(suite, test_buffer_put_string_fit);
     cx_test_register(suite, test_buffer_put_string_discard);
     cx_test_register(suite, test_buffer_put_string_extend);
+    cx_test_register(suite, test_buffer_put_string_extend_copy_on_write);
     cx_test_register(suite, test_buffer_terminate);
     cx_test_register(suite, test_buffer_write_size_overflow);
     cx_test_register(suite, test_buffer_write_capacity_overflow);

mercurial