diff --git a/.clang-format b/.clang-format index f98fb9c..38abf14 100644 --- a/.clang-format +++ b/.clang-format @@ -51,7 +51,7 @@ AllowShortEnumsOnASingleLine: true AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All -AllowShortLoopsOnASingleLine: true +AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: All AlwaysBreakAfterReturnType: AllDefinitions AlwaysBreakBeforeMultilineStrings: false diff --git a/.gitignore b/.gitignore index f25f439..46ca31e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,4 @@ *.o *.s -*.so -*.a build driver -*.elf -*.json -.cache diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml deleted file mode 100644 index 9eece92..0000000 --- a/.woodpecker/test.yaml +++ /dev/null @@ -1,20 +0,0 @@ -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 50d4220..d27a91c 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,22 @@ # 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 -CFLAGS += -I. - -ifneq ($(RELEASE), 1) -CFLAGS += -g -O0 -std=c99 -march=native -mtune=native -CFLAGS += -DDEBUG -else +ifeq ($(RELEASE), 1) 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 -CFLAGS += -fshort-enums -endif # DEBUG +else +CFLAGS += -g -O0 -std=c99 -march=native -mtune=native +CFLAGS += -DDEBUG +endif -LDFLAGS = -lm +# Include debug flags +CFLAGS += -g -O0 -std=c99 -march=native -mtune=native C_SOURCES = $(wildcard *.c) C_HEADERS = $(wildcard *.h) @@ -27,9 +25,6 @@ ASMS = $(C_SOURCES:.c=.s) all: $(OBJECTS) -test: test/test.elf - ./test/test.elf - %.o: %.c $(C_HEADERS) @echo "CC $<" @$(CC) $(CFLAGS) -c $< -o $@ @@ -39,35 +34,19 @@ test: test/test.elf @$(CC) $(CFLAGS) -S -masm=intel $< driver: $(OBJECTS) - @$(CC) $(LDFLAGS) $^ -o $@ + @$(CC) $(CFLAGS) $^ -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 librbuf.a librbuf.so **/*.o **/*.elf + rm -f $(OBJECTS) $(ASMS) driver 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 test +.PHONY: all clean format asm diff --git a/README.md b/README.md deleted file mode 100644 index 1626f2c..0000000 --- a/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# 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 19d0e74..2350cc4 100644 --- a/driver.c +++ b/driver.c @@ -4,100 +4,25 @@ #include #include -#ifndef DEBUG -#define DEBUG -#endif - -// #define rb_size_t size_t -// #define rb_size_t int +#define rb_size_t size_t #include "ringbuf.h" -typedef int DATATYPE; - int main(void) { struct RingBuf rb; + rb_init(&rb, 10, malloc, sizeof(int)); - DATATYPE d; - rb_init(&rb, 10, malloc, sizeof(DATATYPE)); + int data = 5; - rb_debug_print(&rb); + rb_push_back(&rb, &data, memcpy); - const DATATYPE arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - int arrlen = (rb_size_t)(sizeof(arr) / sizeof(DATATYPE)); + int d; + rb_pop_front(&rb, &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"); - } + printf("Data: %d\n", d); 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 59626f7..bd9c26e 100644 --- a/ringbuf.c +++ b/ringbuf.c @@ -1,14 +1,12 @@ /* 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, ...) @@ -25,31 +23,24 @@ 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, FREE_T free_fn) +rb_destroy(struct RingBuf *rb, void(free)()) { - 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; + free(rb->buffer); } enum WriteResult @@ -58,8 +49,6 @@ 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 @@ -72,54 +61,13 @@ 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, MEMCPY_T memcpy_fn) +rb_pop_front(struct RingBuf *rb, void *item) { if(rb->count == 0) return Empty; - memcpy_fn(item, rb->read_head, rb->struct_size); + memcpy(item, rb->read_head, rb->struct_size); // Advance the read head rb->read_head = (char *)rb->read_head + rb->struct_size; @@ -130,37 +78,3 @@ rb_pop_front(struct RingBuf *rb, void *item, MEMCPY_T memcpy_fn) 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 1844437..bffc0f2 100644 --- a/ringbuf.h +++ b/ringbuf.h @@ -2,134 +2,42 @@ #pragma once -#include - #ifndef rb_size_t -#define rb_size_t size_t +#define rb_size_t int #endif -/** Signatures of allocators */ +/** Signatures of generic functions */ 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 *); - /** - * @brief Ring buffer, also known as circular buffer. + * 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)); -/* - * Considerations: One hot encoding for the enum values - * (8 bit each with -fshort-enums) - */ +// TODO: Perhaps unify these to RBResult? -/** Result of a write */ -enum WriteResult { - WriteOk, /** The write was successful */ - Full, /** The buffer is full */ - InsufficientSpace /** There is not enough space to write */ -}; +enum WriteResult { Full, WriteOk }; /** Result of a write */ +enum ReadResult { Empty, ReadOk }; /** Result of a read */ -/** 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, +/** Initialize the ring buffer */ +void rb_init(struct RingBuf *rb, rb_size_t capacity, void *(*alloc)(rb_size_t), rb_size_t struct_size); -/** - * @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 - */ +/** Insert data to the ring buffer */ enum WriteResult rb_push_back(struct RingBuf *rb, const void *item, MEMCPY_T memcpy_fn); -/** - * @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); +/** Read data from the ring buffer */ +enum ReadResult rb_pop_front(struct RingBuf *rb, void *item); -/** - * @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 - */ +/** Free the ring buffer */ 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 deleted file mode 100644 index 44756e9..0000000 --- a/test/test_ringbuf.c +++ /dev/null @@ -1,101 +0,0 @@ -#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); -}