Sun, 13 Apr 2025 13:02:54 +0200
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);