Wed, 18 Dec 2024 15:35:42 +0100
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);