diff --git a/.clang-format b/.clang-format index 38abf14..f98fb9c 100644 --- a/.clang-format +++ b/.clang-format @@ -51,7 +51,7 @@ AllowShortEnumsOnASingleLine: true AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All -AllowShortLoopsOnASingleLine: false +AllowShortLoopsOnASingleLine: true AlwaysBreakAfterDefinitionReturnType: All AlwaysBreakAfterReturnType: AllDefinitions AlwaysBreakBeforeMultilineStrings: false diff --git a/.gitignore b/.gitignore index 46ca31e..f25f439 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ *.o *.s +*.so +*.a build driver +*.elf +*.json +.cache diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml new file mode 100644 index 0000000..9eece92 --- /dev/null +++ b/.woodpecker/test.yaml @@ -0,0 +1,20 @@ +when: + - event: push + branch: master + +steps: + - name: build + image: debian + commands: + - apt update + - apt install libcmocka-dev -y + - apt install gcc -y + - apt install make -y + - make -j$(nproc) test/test.elf + + - name: run-tests + image: debian + commands: + - apt update + - apt install libcmocka0 -y + - ./test/test.elf diff --git a/Makefile b/Makefile index d27a91c..50d4220 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,24 @@ # SPDX-License-Identifier: MIT CC = gcc -CFLAGS = -Wall -Wextra -Werror -Wno-unused-parameter -CFLAGS += -Wno-unused-variable -Wno-unused-function +CFLAGS = -Wall -Wextra -Werror -Wno-unused-parameter +CFLAGS += -Wno-unused-variable -Wno-unused-function CFLAGS += -Wno-unused-but-set-variable -Wno-unused-value -Wno-unused-label CFLAGS += -Wno-unused-result -Wno-unused-const-variable -ifeq ($(RELEASE), 1) +CFLAGS += -I. + +ifneq ($(RELEASE), 1) +CFLAGS += -g -O0 -std=c99 -march=native -mtune=native +CFLAGS += -DDEBUG +else CFLAGS += -fno-exceptions -fno-asynchronous-unwind-tables -fno-ident CFLAGS += -fno-unwind-tables -fno-stack-protector -fno-plt -fno-pic CFLAGS += -O3 -std=c99 -march=native -mtune=native -fomit-frame-pointer -else -CFLAGS += -g -O0 -std=c99 -march=native -mtune=native -CFLAGS += -DDEBUG -endif +CFLAGS += -fshort-enums +endif # DEBUG -# Include debug flags -CFLAGS += -g -O0 -std=c99 -march=native -mtune=native +LDFLAGS = -lm C_SOURCES = $(wildcard *.c) C_HEADERS = $(wildcard *.h) @@ -25,6 +27,9 @@ ASMS = $(C_SOURCES:.c=.s) all: $(OBJECTS) +test: test/test.elf + ./test/test.elf + %.o: %.c $(C_HEADERS) @echo "CC $<" @$(CC) $(CFLAGS) -c $< -o $@ @@ -34,19 +39,35 @@ all: $(OBJECTS) @$(CC) $(CFLAGS) -S -masm=intel $< driver: $(OBJECTS) - @$(CC) $(CFLAGS) $^ -o $@ + @$(CC) $(LDFLAGS) $^ -o $@ run: driver @./driver +test/test.elf: test/test_ringbuf.o ringbuf.o + @$(CC) $(CFLAGS) $^ -o $@ -lpthread -lcmocka + +lib: $(OBJECTS) + @ar rcs librbuf.a ringbuf.o + +dylib: $(OBJECTS) + @$(CC) $(LDLAGS) -fPIC -shared -o librbuf.so ringbuf.o + +install: + @cp librbuf.a /usr/local/lib + @cp ringbuf.h /usr/local/include + clean: - rm -f $(OBJECTS) $(ASMS) driver + rm -f $(OBJECTS) $(ASMS) driver librbuf.a librbuf.so **/*.o **/*.elf asm: $(ASMS) $(OBJECTS) wc -l $(ASMS) size $(OBJECTS) +tidy: + @clang-tidy $(C_SOURCES) -- $(CFLAGS) + format: clang-format -i $(C_SOURCES) $(C_HEADERS) -.PHONY: all clean format asm +.PHONY: all clean format asm test diff --git a/README.md b/README.md new file mode 100644 index 0000000..1626f2c --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# RingBuf + +RingBuf is an allocator-agnostic, non-overwriting circular/ring buffer +implementation in C99. +See: [Circular Buffer (Wikipedia)](https://en.wikipedia.org/wiki/Circular_buffer) + +## Features +- Space Efficiency +The code is designed to be portable and flexible. The inspiration initially +came to me when designing a network driver. When operating in memory +constrained environments, every byte is of upmost importance. Traditional +metaprogramming such as templating in C++ and template metaprogramming in C, +although generic has the side effect of expanding into discrete machine code +specific for the specialization applied, essentially scaling (spatially) linear +with the amount of different datatypes we specialize on. This implementation +circumvents this by treating all data as a void pointer with a length. This +implies **no deep copies**. + +- Allocator Agnostic +Another common characteristic of memory constrained environments are +custom malloc implementations and/or variations. + - Signatures + - Arena + +## Design considerations +- Holding a reference to malloc internally would make a tidier interface +- Passing a user-defined function for deep-copies would enable certain applications + +## Usage +In essence: +```c +int value = 42; +struct RingBuf rb; +rb_init(&rb, 10, malloc, sizeof(int)); +rb_push_back(&rb, (void *)&data, memcpy); +rb_pop_front(&rb, &value, memcpy); +``` +Most of these functions return Enum result types. See: +[ringbuf.h](./ringbuf.h). + +## Future Improvements +- Trim the code +- Reduce boilerplate in tests +- Reduce the number of tests in exchange for better test fit diff --git a/driver.c b/driver.c index 2350cc4..19d0e74 100644 --- a/driver.c +++ b/driver.c @@ -4,25 +4,100 @@ #include #include -#define rb_size_t size_t +#ifndef DEBUG +#define DEBUG +#endif + +// #define rb_size_t size_t +// #define rb_size_t int #include "ringbuf.h" +typedef int DATATYPE; + int main(void) { struct RingBuf rb; - rb_init(&rb, 10, malloc, sizeof(int)); - int data = 5; + DATATYPE d; + rb_init(&rb, 10, malloc, sizeof(DATATYPE)); - rb_push_back(&rb, &data, memcpy); + rb_debug_print(&rb); - int d; - rb_pop_front(&rb, &d); + const DATATYPE arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + int arrlen = (rb_size_t)(sizeof(arr) / sizeof(DATATYPE)); - printf("Data: %d\n", d); + // Single writes + printf("\n=== Single writes ===\n\n"); + + rb_debug_print(&rb); + + int idx = 0; + while(idx < 5) { + if(rb_push_back(&rb, &arr[idx], memcpy) != WriteOk) { + printf("Failed to write data to buffer...\n"); + } + idx++; + } + + rb_debug_print(&rb); + + // Pop the last n elements + for(int a = 0; a < 10; a++) { + if(rb_pop_front(&rb, &d, memcpy) != ReadOk) { + printf("Failed to read data from buffer...\n"); + break; + } + printf("Data: %d\n", d); + } + + rb_debug_print(&rb); + if(rb.read_head == rb.write_head) + printf("OK\n"); + printf("idx: %d\n", idx); + + // Push the rest + while(idx < arrlen && rb_push_back(&rb, &arr[idx], memcpy) == WriteOk) { + idx++; + } + + rb_debug_print(&rb); + + printf("Data: ["); + while(rb_pop_front(&rb, &d, memcpy) == ReadOk) + printf("%d,", d); + printf("\b]\n"); + + // Multiple writes + printf("\n=== Multiple writes ===\n\n"); + + rb_clear(&rb); // Make sure + rb_debug_print(&rb); + + int ok = WriteOk; // Assume we can write + if(rb_push_many(&rb, arr, memcpy, 8) != WriteOk) { + printf("Failed to write data to buffer...\n"); + } + rb_debug_print(&rb); + rb_debug_empty(&rb); + + // Test wrap around + rb_push_many(&rb, arr, memcpy, 10); + rb_debug_print(&rb); + rb_debug_empty(&rb); + + // Test clear + rb_clear(&rb); + if(rb_pop_front(&rb, &d, memcpy) != Empty) { + printf("Buffer is not empty after clear...\n"); + } rb_destroy(&rb, free); + enum WriteResult wr = WriteOk; + enum ReadResult rr = ReadOk; + printf("Size of wr: %lu bytes.\n", sizeof(wr)); + printf("Size of rr: %lu bytes.\n", sizeof(rr)); + return 0; } diff --git a/ringbuf.c b/ringbuf.c index bd9c26e..59626f7 100644 --- a/ringbuf.c +++ b/ringbuf.c @@ -1,12 +1,14 @@ /* SPDX-License-Identifier: MIT */ -#include -#include -#include +// #include +// #include #include "ringbuf.h" +#include +#include #ifdef DEBUG +#include #define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__) #else #define DEBUG_PRINT(fmt, ...) @@ -23,24 +25,31 @@ rb_init(struct RingBuf *rb, rb_size_t capacity, ALLOC_T malloc_fn, rb->count = 0; rb->write_head = rb->buffer; rb->read_head = rb->buffer; - - // Read from buffer at max position to force a segfault if theres an issue - DEBUG_PRINT("Reading from buffer at position %d\n", - rb->capacity * rb->struct_size); - void *top = rb->buffer + (rb->capacity * rb->struct_size); - DEBUG_PRINT("Buffer top successfully read at virtual address: %p\n", &top); - - DEBUG_PRINT( - "Initialized ring buffer. Capacit: %d, struct_size: %d, total: %d\n", - rb->capacity, rb->struct_size, rb->capacity * rb->struct_size); - - DEBUG_PRINT("Size of RB: %lu\n", sizeof(struct RingBuf)); } void -rb_destroy(struct RingBuf *rb, void(free)()) +rb_destroy(struct RingBuf *rb, FREE_T free_fn) { - free(rb->buffer); + if(rb->buffer) { // Prevent double-free + free_fn(rb->buffer); + rb->buffer = NULL; + rb->buffer_end = NULL; + + rb->struct_size = 0; + rb->capacity = 0; + rb->count = 0; + + rb->write_head = NULL; + rb->read_head = NULL; + } +} + +void +rb_clear(struct RingBuf *rb) +{ + rb->count = 0; + rb->write_head = rb->buffer; + rb->read_head = rb->buffer; } enum WriteResult @@ -49,6 +58,8 @@ rb_push_back(struct RingBuf *rb, const void *item, MEMCPY_T memcpy_fn) if(rb->count == rb->capacity) return Full; + assert(rb->buffer != NULL); + memcpy_fn(rb->write_head, item, rb->struct_size); // Advance the write head @@ -61,13 +72,54 @@ rb_push_back(struct RingBuf *rb, const void *item, MEMCPY_T memcpy_fn) return WriteOk; } +enum WriteResult +rb_push_many(struct RingBuf *rb, const void *items, MEMCPY_T memcpy_fn, + rb_size_t n) +{ + if(rb->count + n > rb->capacity) + return Full; // Perhaps rename to InsufficientSpace + + // If the write head will move past the end of the buffer + // we need to to the write in two steps. + void *end = (char *)rb->write_head + rb->struct_size * n; + + if(end > rb->buffer_end) { + + // Calculate the number of items that can be written in the first chunk + rb_size_t first_chunk = (char *)rb->buffer_end - (char *)rb->write_head; + + DEBUG_PRINT("Multi-chunk write. First chunk: %ld\n", first_chunk); + + // Write the first chunk + memcpy_fn(rb->write_head, items, rb->struct_size * first_chunk); + + // Set the write head to the beginning of the buffer + rb->write_head = rb->buffer; + rb->count += first_chunk; + n -= first_chunk; + } else { + DEBUG_PRINT("Single-chunk write. No need to wrap around.%s\n", ""); + } + + DEBUG_PRINT("Writing %ld items\n", n); + memcpy_fn(rb->write_head, items, rb->struct_size * n); + if(rb->write_head == rb->buffer_end) + rb->write_head = rb->buffer; + + // Advance the write head + rb->write_head = (char *)rb->write_head + rb->struct_size * n; + rb->count += n; + + return WriteOk; +} + enum ReadResult -rb_pop_front(struct RingBuf *rb, void *item) +rb_pop_front(struct RingBuf *rb, void *item, MEMCPY_T memcpy_fn) { if(rb->count == 0) return Empty; - memcpy(item, rb->read_head, rb->struct_size); + memcpy_fn(item, rb->read_head, rb->struct_size); // Advance the read head rb->read_head = (char *)rb->read_head + rb->struct_size; @@ -78,3 +130,37 @@ rb_pop_front(struct RingBuf *rb, void *item) rb->count--; return ReadOk; } + +#ifdef DEBUG +#include + +void +rb_debug_empty(struct RingBuf *rb) +{ + int d; + if(rb->count == 0) + return; + printf("Debug Data: ["); + while(rb_pop_front(rb, &d, memcpy) == ReadOk) + printf("%d,", d); + printf("\b]\n"); +} + +void +rb_debug_print(struct RingBuf *rb) +{ + DEBUG_PRINT("============%s\n", ""); + DEBUG_PRINT("Count %lu\n", rb->count); + DEBUG_PRINT("Capacity: %ld\n", rb->capacity); + DEBUG_PRINT("Left: %ld\n", rb->capacity - rb->count); + DEBUG_PRINT("Base addr:\t%p\n", rb->buffer); + + DEBUG_PRINT("Read Head:\t%p (%ld:th position)\n", rb->read_head, + ((rb->read_head) - (rb->buffer)) / rb->struct_size); + DEBUG_PRINT("Write Head:\t%p (%ld:th position)\n", rb->write_head, + ((rb->write_head) - (rb->buffer)) / rb->struct_size); + + DEBUG_PRINT("============%s\n", ""); +} + +#endif diff --git a/ringbuf.h b/ringbuf.h index bffc0f2..1844437 100644 --- a/ringbuf.h +++ b/ringbuf.h @@ -2,42 +2,134 @@ #pragma once +#include + #ifndef rb_size_t -#define rb_size_t int +#define rb_size_t size_t #endif -/** Signatures of generic functions */ +/** Signatures of allocators */ typedef void *(*ALLOC_T)(rb_size_t); + +/** Signature of memcpy */ typedef void *(*MEMCPY_T)(void *, const void *, rb_size_t); +/** Signature of free */ +typedef void (*FREE_T)(void *); + /** - * Ring buffer, also known as circular buffer. + * @brief Ring buffer, also known as circular buffer. */ struct RingBuf { - rb_size_t struct_size; /* Size of the struct */ - rb_size_t capacity; /* The physical capacity of the entire ringbuf */ - rb_size_t count; /* The number of elements in the buffer */ - void *write_head; /* Address of the write head */ - void *read_head; /* Address of the read head */ - void *buffer; /* The actual data */ - void *buffer_end; /* The end of the buffer */ + rb_size_t struct_size; /** Size of the struct */ + rb_size_t capacity; /** The physical capacity of the entire ringbuf */ + rb_size_t count; /** The number of elements in the buffer */ + void *write_head; /** Address of the write head */ + void *read_head; /** Address of the read head */ + void *buffer; /** The actual data */ + void *buffer_end; /** The end of the buffer */ } __attribute__((packed)); -// TODO: Perhaps unify these to RBResult? +/* + * Considerations: One hot encoding for the enum values + * (8 bit each with -fshort-enums) + */ -enum WriteResult { Full, WriteOk }; /** Result of a write */ -enum ReadResult { Empty, ReadOk }; /** Result of a read */ +/** Result of a write */ +enum WriteResult { + WriteOk, /** The write was successful */ + Full, /** The buffer is full */ + InsufficientSpace /** There is not enough space to write */ +}; -/** Initialize the ring buffer */ -void rb_init(struct RingBuf *rb, rb_size_t capacity, void *(*alloc)(rb_size_t), +/** Result of a read */ +enum ReadResult { + Empty, /** The buffer is empty */ + ReadOk, /** The read was successful */ + InsufficientData /** There is not enough data to read */ +}; + +/** + * @brief Initialize the ring buffer + * @param rb The ring buffer to initialize + * @param capacity The capacity of the ring buffer + * @param alloc The allocator function + * @param struct_size The size of the struct + * @return void + */ +void rb_init(struct RingBuf *rb, rb_size_t capacity, ALLOC_T alloc, rb_size_t struct_size); -/** Insert data to the ring buffer */ +/** + * @brief Clear the ring buffer + * @details This function will reset the read and write heads to the beginning + * of the buffer, and set the count to 0. It will not free the buffer. + * @param rb The ring buffer + */ +void rb_clear(struct RingBuf *rb); + +/** + * @brief Insert data to the ring buffer + * @param rb The ring buffer + * @param item The item to insert + * @param memcpy_fn The memcpy function + * @return WriteResult + */ enum WriteResult rb_push_back(struct RingBuf *rb, const void *item, MEMCPY_T memcpy_fn); -/** Read data from the ring buffer */ -enum ReadResult rb_pop_front(struct RingBuf *rb, void *item); +/** + * @brief Insert multiple data to the ring buffer + * + * @details This function is more efficient than calling rb_push_back multiple + * times. It only advances the write head once, and attempts to write all the + * memory in one go. + * + * If n is greater than the capacity, it will return Full. + * If the full write will overflow, it will wrap around. + * + * If the buffer is full, it will return Full and not write + * anything. + * + * @param rb The ring buffer + * @param items The items to insert + * @param memcpy_fn The memcpy function + * @param n The number of items to insert + * @return WriteResult + */ +enum WriteResult rb_push_many(struct RingBuf *rb, const void *items, + MEMCPY_T memcpy_fn, rb_size_t n); -/** Free the ring buffer */ +/** + * @brief Read data from the ring buffer + * @param rb The ring buffer + * @param item The item to read into + * @return ReadResult + */ +enum ReadResult rb_pop_front(struct RingBuf *rb, void *item, + MEMCPY_T memcpy_fn); + +/** + * @brief Free the ring buffer + * + * @details This function is idempotent, consecutive calls will not result in a + * double free. + * + * @param rb The ring buffer + * @param free The free function + */ void rb_destroy(struct RingBuf *rb, void(free)()); + +#ifdef DEBUG + +/** + * @brief Debug print + */ +void rb_debug_print(struct RingBuf *rb); + +/** + * @brief Debug print and empty the ringbuf + */ +void rb_debug_empty(struct RingBuf *rb); + +#endif diff --git a/test/test_ringbuf.c b/test/test_ringbuf.c new file mode 100644 index 0000000..44756e9 --- /dev/null +++ b/test/test_ringbuf.c @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include +#include +#include "ringbuf.h" + +/** Tests initialization */ +static void +test_rb_init_empty(void **state) { + struct RingBuf rb; + rb_size_t capacity = 10; + rb_size_t struct_size = sizeof(int); + + rb_init(&rb, capacity, malloc, struct_size); + + assert_int_equal(rb.capacity, capacity); + assert_int_equal(rb.count, 0); +} + +/** Tests push_back */ +static void +test_rb_push_back(void **state) { + struct RingBuf rb; + rb_size_t capacity = 10; + rb_size_t struct_size = sizeof(int); + + rb_init(&rb, capacity, malloc, struct_size); + + int data = 10; + + enum WriteResult wr = rb_push_back(&rb, (void *)&data, memcpy); + assert_int_equal(wr, WriteOk); + + assert_int_equal(rb.capacity, capacity); + assert_int_equal(rb.count, 1); +} + +/** Tests the destroy function */ +static void +test_rb_init_destroy(void **state) { + struct RingBuf rb = { 0, 0, 0, NULL, NULL, NULL, NULL }; + rb_size_t capacity = 10; + rb_size_t struct_size = sizeof(int); + + rb_init(&rb, capacity, malloc, struct_size); + + int data = 10; + + rb_push_back(&rb, (void *)&data, memcpy); + + assert_int_equal(rb.capacity, capacity); + assert_int_equal(rb.count, 1); + + rb_destroy(&rb, free); + + assert_null(rb.buffer); +} + +/** Tests fill, but not overflow/wrap-around */ +static void +test_rb_push_back_fill(void **state) { + struct RingBuf rb = { 0, 0, 0, NULL, NULL, NULL, NULL }; + rb_size_t capacity = 10; + rb_size_t struct_size = sizeof(int); + + rb_init(&rb, capacity, malloc, struct_size); + + const int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + + // Insert them all sequentially + for(int i = 0; rb_push_back(&rb, &arr[i], memcpy) == WriteOk; i++); + assert_int_equal(rb.count, 10); + assert_int_equal(rb.capacity, capacity); + + // Read them out and assert expected value + for(int i = 0, d = __INT_MAX__; rb_pop_front(&rb, &d, memcpy) == ReadOk; + i++) { + assert_int_equal(d, arr[i]); + } + + assert_int_equal(rb.capacity, capacity); + assert_int_equal(rb.count, 0); + + rb_destroy(&rb, free); + + assert_null(rb.buffer); +} + +int +main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_rb_init_empty), + cmocka_unit_test(test_rb_push_back), + cmocka_unit_test(test_rb_init_destroy), + cmocka_unit_test(test_rb_push_back_fill), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}