adds cxBufferShrink() - resolves #626

Sun, 13 Apr 2025 13:02:54 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 13 Apr 2025 13:02:54 +0200
changeset 1290
4ac889e14211
parent 1289
2e8edba252a0
child 1291
5942859fd76c

adds cxBufferShrink() - resolves #626

CHANGELOG file | annotate | diff | comparison | revisions
docs/Writerside/topics/about.md file | annotate | diff | comparison | revisions
docs/Writerside/topics/buffer.h.md file | annotate | diff | comparison | revisions
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/CHANGELOG	Sun Apr 13 12:30:18 2025 +0200
+++ b/CHANGELOG	Sun Apr 13 13:02:54 2025 +0200
@@ -3,6 +3,7 @@
 
  * adds cxMempoolTransfer() and cxMempoolTransferObject()
  * adds cxListSet()
+ * adds cxBufferShrink()
  * changes grow strategy for the mempory pool to reduce reallocations
  * fixes errno value after failing cxBufferSeek() to be consistently EINVAL
  * fixes implementation of cxBufferTerminate()
--- a/docs/Writerside/topics/about.md	Sun Apr 13 12:30:18 2025 +0200
+++ b/docs/Writerside/topics/about.md	Sun Apr 13 13:02:54 2025 +0200
@@ -30,6 +30,7 @@
 
 * adds cxMempoolTransfer() and cxMempoolTransferObject()
 * adds cxListSet()
+* adds cxBufferShrink()
 * changes grow strategy for the mempory pool to reduce reallocations
 * fixes errno value after failing cxBufferSeek() to be consistently EINVAL
 * fixes implementation of cxBufferTerminate()
--- a/docs/Writerside/topics/buffer.h.md	Sun Apr 13 12:30:18 2025 +0200
+++ b/docs/Writerside/topics/buffer.h.md	Sun Apr 13 13:02:54 2025 +0200
@@ -117,6 +117,29 @@
 and the function `cxBufferFree()` is to be used when the buffer was created with `cxBufferCreate()`.
 The only difference is, that `cxBufferFree()` additionally deallocates the memory of the `CxBuffer` structure. 
 
+## Capacity Management
+
+```C
+#include <cx/buffer.h>
+
+int cxBufferMinimumCapacity(CxBuffer *buffer, size_t capacity);
+
+void cxBufferShrink(CxBuffer *buffer, size_t reserve);
+```
+
+The function `cxBufferMinimumCapacity()` guarantees a buffer capacity of _at least_ `capacity`.
+It may allocate more space, depending on the allocation strategy.
+The function returns non-zero if increasing the capacity was attempted unsuccessfully.
+
+The function `cxBufferShrink()` can be used to shrink the capacity of the buffer to its current size
+plus a number of `reserve` bytes.
+If the current capacity is not larger than the size plus the reserve bytes, the function will do nothing.
+
+> If the buffer is in a copy-on-write state, `cxBufferMinimumCapacity()` will perform the copy-on-write action
+> before reallocating the space.
+> The function `cxBufferShrink()` on the other hand does _nothing_ when the buffer is in a copy-on-write state,
+> because it does not make much sense to copy the memory just to have it in a smaller memory region.
+
 ## Write
 
 ```C
@@ -133,8 +156,6 @@
 
 size_t cxBufferAppend(const void *ptr, size_t size, size_t nitems,
         CxBuffer *buffer);
-
-int cxBufferMinimumCapacity(CxBuffer *buffer, size_t capacity);
 ```
 
 The primary function for writing to a buffer is `cxBufferWrite()`
@@ -163,10 +184,6 @@
 If the write operation triggered a [flush](#flushing), however, the position will be shifted left alongside the shifted buffer contents.
 In case the data at which the current position points gets flushed, the new position will be zero.
 
-If you already (roughly) know how many bytes you will be writing to a buffer,
-you can save some allocations during auto-extension when you invoke `cxBufferMinimumCapacity()` before writing the data.
-Usually you do not need to do this, unless you have many subsequence writes which impose the risk of multiple unnecessary reallocations.
-
 ## Read
 
 ```C
--- a/src/buffer.c	Sun Apr 13 12:30:18 2025 +0200
+++ b/src/buffer.c	Sun Apr 13 13:02:54 2025 +0200
@@ -209,6 +209,28 @@
     }
 }
 
+void cxBufferShrink(
+        CxBuffer *buffer,
+        size_t reserve
+) {
+    // Ensure buffer is in a reallocatable state
+    const int force_copy_flags = CX_BUFFER_COPY_ON_WRITE | CX_BUFFER_COPY_ON_EXTEND;
+    if (buffer->flags & force_copy_flags) {
+        // do nothing when we are not allowed to reallocate
+        return;
+    }
+
+    // calculate new capacity
+    size_t newCapacity = buffer->size + reserve;
+
+    // If new capacity is smaller than current capacity, resize the buffer
+    if (newCapacity < buffer->capacity) {
+        if (0 == cxReallocate(buffer->allocator, &buffer->bytes, newCapacity)) {
+            buffer->capacity = newCapacity;
+        }
+    }
+}
+
 static size_t cx_buffer_flush_helper(
         const CxBuffer *buffer,
         const unsigned char *src,
--- a/src/cx/buffer.h	Sun Apr 13 12:30:18 2025 +0200
+++ b/src/cx/buffer.h	Sun Apr 13 13:02:54 2025 +0200
@@ -478,6 +478,7 @@
  * @param capacity the minimum required capacity for this buffer
  * @retval zero the capacity was already sufficient or successfully increased
  * @retval non-zero on allocation failure
+ * @see cxBufferShrink()
  */
 cx_attr_nonnull
 cx_attr_export
@@ -487,6 +488,29 @@
 );
 
 /**
+ * Shrinks the capacity of the buffer to fit its current size.
+ *
+ * If @p reserve is larger than zero, the buffer is shrunk to its size plus
+ * the number of reserved bytes.
+ *
+ * If the current capacity is not larger than the size plus the reserved bytes,
+ * nothing happens.
+ *
+ * If the #CX_BUFFER_COPY_ON_WRITE or #CX_BUFFER_COPY_ON_EXTEND flag is set,
+ * this function does nothing.
+ *
+ * @param buffer the buffer
+ * @param reserve the number of bytes that shall remain reserved
+ * @see cxBufferMinimumCapacity()
+ */
+cx_attr_nonnull
+cx_attr_export
+void cxBufferShrink(
+        CxBuffer *buffer,
+        size_t reserve
+);
+
+/**
  * Writes data to a CxBuffer.
  *
  * If automatic flushing is not enabled, the data is simply written into the
--- a/tests/test_buffer.c	Sun Apr 13 12:30:18 2025 +0200
+++ b/tests/test_buffer.c	Sun Apr 13 13:02:54 2025 +0200
@@ -187,6 +187,69 @@
     cx_testing_allocator_destroy(&talloc);
 }
 
+CX_TEST(test_buffer_shrink) {
+    CxTestingAllocator talloc;
+    cx_testing_allocator_init(&talloc);
+    CxAllocator *alloc = &talloc.base;
+    CX_TEST_DO {
+        CxBuffer buf;
+        cxBufferInit(&buf, NULL, 16, alloc, CX_BUFFER_FREE_CONTENTS);
+        cxBufferPutString(&buf, "Testing");
+        cxBufferTerminate(&buf);
+        CX_TEST_ASSERT(buf.capacity == 16);
+        CX_TEST_ASSERT(buf.size == 7);
+        cxBufferShrink(&buf, 4);
+        CX_TEST_ASSERT(buf.capacity == 11);
+        CX_TEST_ASSERT(buf.size == 7);
+        CX_TEST_ASSERT(memcmp(buf.space, "Testing", 8) == 0);
+        cxBufferDestroy(&buf);
+        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
+    }
+    cx_testing_allocator_destroy(&talloc);
+}
+
+CX_TEST(test_buffer_shrink_copy_on_write) {
+    CxTestingAllocator talloc;
+    cx_testing_allocator_init(&talloc);
+    CxAllocator *alloc = &talloc.base;
+    CX_TEST_DO {
+        CxBuffer buf;
+        const char* space = "Testing";
+        cxBufferInit(&buf, (void*)space, 16, alloc, CX_BUFFER_COPY_ON_WRITE);
+        buf.size = 8;
+        CX_TEST_ASSERT(buf.capacity == 16);
+        cxBufferShrink(&buf, 4);
+        CX_TEST_ASSERT(buf.capacity == 16);
+        CX_TEST_ASSERT(buf.size == 8);
+        CX_TEST_ASSERT(memcmp(buf.space, "Testing", 8) == 0);
+        CX_TEST_ASSERT(talloc.alloc_total == 0);
+        cxBufferDestroy(&buf);
+        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
+    }
+    cx_testing_allocator_destroy(&talloc);
+}
+
+CX_TEST(test_buffer_shrink_copy_on_extend) {
+    CxTestingAllocator talloc;
+    cx_testing_allocator_init(&talloc);
+    CxAllocator *alloc = &talloc.base;
+    CX_TEST_DO {
+        CxBuffer buf;
+        char space[16] = "Testing";
+        cxBufferInit(&buf, space, 16, alloc, CX_BUFFER_COPY_ON_EXTEND);
+        buf.size = 8;
+        CX_TEST_ASSERT(buf.capacity == 16);
+        cxBufferShrink(&buf, 4);
+        CX_TEST_ASSERT(buf.capacity == 16);
+        CX_TEST_ASSERT(buf.size == 8);
+        CX_TEST_ASSERT(memcmp(buf.space, "Testing", 8) == 0);
+        CX_TEST_ASSERT(talloc.alloc_total == 0);
+        cxBufferDestroy(&buf);
+        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
+    }
+    cx_testing_allocator_destroy(&talloc);
+}
+
 CX_TEST(test_buffer_clear) {
     char space[16];
     strcpy(space, "clear test");
@@ -1505,6 +1568,9 @@
     cx_test_register(suite, test_buffer_init_on_heap);
     cx_test_register(suite, test_buffer_minimum_capacity_sufficient);
     cx_test_register(suite, test_buffer_minimum_capacity_extend);
+    cx_test_register(suite, test_buffer_shrink);
+    cx_test_register(suite, test_buffer_shrink_copy_on_write);
+    cx_test_register(suite, test_buffer_shrink_copy_on_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);

mercurial