Compare commits

..

No commits in common. "master" and "0.0.1" have entirely different histories.

9 changed files with 58 additions and 502 deletions

View file

@ -51,7 +51,7 @@ AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: true AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: All AlwaysBreakAfterDefinitionReturnType: All
AlwaysBreakAfterReturnType: AllDefinitions AlwaysBreakAfterReturnType: AllDefinitions
AlwaysBreakBeforeMultilineStrings: false AlwaysBreakBeforeMultilineStrings: false

5
.gitignore vendored
View file

@ -1,9 +1,4 @@
*.o *.o
*.s *.s
*.so
*.a
build build
driver driver
*.elf
*.json
.cache

View file

@ -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

View file

@ -6,19 +6,17 @@ CFLAGS += -Wno-unused-variable -Wno-unused-function
CFLAGS += -Wno-unused-but-set-variable -Wno-unused-value -Wno-unused-label CFLAGS += -Wno-unused-but-set-variable -Wno-unused-value -Wno-unused-label
CFLAGS += -Wno-unused-result -Wno-unused-const-variable CFLAGS += -Wno-unused-result -Wno-unused-const-variable
CFLAGS += -I. ifeq ($(RELEASE), 1)
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-exceptions -fno-asynchronous-unwind-tables -fno-ident
CFLAGS += -fno-unwind-tables -fno-stack-protector -fno-plt -fno-pic CFLAGS += -fno-unwind-tables -fno-stack-protector -fno-plt -fno-pic
CFLAGS += -O3 -std=c99 -march=native -mtune=native -fomit-frame-pointer CFLAGS += -O3 -std=c99 -march=native -mtune=native -fomit-frame-pointer
CFLAGS += -fshort-enums else
endif # DEBUG 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_SOURCES = $(wildcard *.c)
C_HEADERS = $(wildcard *.h) C_HEADERS = $(wildcard *.h)
@ -27,9 +25,6 @@ ASMS = $(C_SOURCES:.c=.s)
all: $(OBJECTS) all: $(OBJECTS)
test: test/test.elf
./test/test.elf
%.o: %.c $(C_HEADERS) %.o: %.c $(C_HEADERS)
@echo "CC $<" @echo "CC $<"
@$(CC) $(CFLAGS) -c $< -o $@ @$(CC) $(CFLAGS) -c $< -o $@
@ -39,35 +34,19 @@ test: test/test.elf
@$(CC) $(CFLAGS) -S -masm=intel $< @$(CC) $(CFLAGS) -S -masm=intel $<
driver: $(OBJECTS) driver: $(OBJECTS)
@$(CC) $(LDFLAGS) $^ -o $@ @$(CC) $(CFLAGS) $^ -o $@
run: driver run: driver
@./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: clean:
rm -f $(OBJECTS) $(ASMS) driver librbuf.a librbuf.so **/*.o **/*.elf rm -f $(OBJECTS) $(ASMS) driver
asm: $(ASMS) $(OBJECTS) asm: $(ASMS) $(OBJECTS)
wc -l $(ASMS) wc -l $(ASMS)
size $(OBJECTS) size $(OBJECTS)
tidy:
@clang-tidy $(C_SOURCES) -- $(CFLAGS)
format: format:
clang-format -i $(C_SOURCES) $(C_HEADERS) clang-format -i $(C_SOURCES) $(C_HEADERS)
.PHONY: all clean format asm test .PHONY: all clean format asm

View file

@ -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

View file

@ -4,100 +4,25 @@
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#ifndef DEBUG #define rb_size_t size_t
#define DEBUG
#endif
// #define rb_size_t size_t
// #define rb_size_t int
#include "ringbuf.h" #include "ringbuf.h"
typedef int DATATYPE;
int int
main(void) main(void)
{ {
struct RingBuf rb; struct RingBuf rb;
rb_init(&rb, 10, malloc, sizeof(int));
DATATYPE d; int data = 5;
rb_init(&rb, 10, malloc, sizeof(DATATYPE));
rb_debug_print(&rb); rb_push_back(&rb, &data, memcpy);
const DATATYPE arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int d;
int arrlen = (rb_size_t)(sizeof(arr) / sizeof(DATATYPE)); rb_pop_front(&rb, &d);
// Single writes printf("Data: %d\n", d);
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); 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; return 0;
} }

124
ringbuf.c
View file

@ -1,14 +1,12 @@
/* SPDX-License-Identifier: MIT */ /* SPDX-License-Identifier: MIT */
// #include <stdlib.h> #include <stdlib.h>
// #include <string.h> #include <string.h>
#include <stdio.h>
#include "ringbuf.h" #include "ringbuf.h"
#include <assert.h>
#include <stdint.h>
#ifdef DEBUG #ifdef DEBUG
#include <stdio.h>
#define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__) #define DEBUG_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
#else #else
#define DEBUG_PRINT(fmt, ...) #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->count = 0;
rb->write_head = rb->buffer; rb->write_head = rb->buffer;
rb->read_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 void
rb_destroy(struct RingBuf *rb, FREE_T free_fn) rb_destroy(struct RingBuf *rb, void(free)())
{ {
if(rb->buffer) { // Prevent double-free free(rb->buffer);
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 enum WriteResult
@ -58,8 +49,6 @@ rb_push_back(struct RingBuf *rb, const void *item, MEMCPY_T memcpy_fn)
if(rb->count == rb->capacity) if(rb->count == rb->capacity)
return Full; return Full;
assert(rb->buffer != NULL);
memcpy_fn(rb->write_head, item, rb->struct_size); memcpy_fn(rb->write_head, item, rb->struct_size);
// Advance the write head // Advance the write head
@ -72,54 +61,13 @@ rb_push_back(struct RingBuf *rb, const void *item, MEMCPY_T memcpy_fn)
return WriteOk; 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 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) if(rb->count == 0)
return Empty; return Empty;
memcpy_fn(item, rb->read_head, rb->struct_size); memcpy(item, rb->read_head, rb->struct_size);
// Advance the read head // Advance the read head
rb->read_head = (char *)rb->read_head + rb->struct_size; 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--; rb->count--;
return ReadOk; return ReadOk;
} }
#ifdef DEBUG
#include <stdio.h>
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

130
ringbuf.h
View file

@ -2,134 +2,42 @@
#pragma once #pragma once
#include <string.h>
#ifndef rb_size_t #ifndef rb_size_t
#define rb_size_t size_t #define rb_size_t int
#endif #endif
/** Signatures of allocators */ /** Signatures of generic functions */
typedef void *(*ALLOC_T)(rb_size_t); typedef void *(*ALLOC_T)(rb_size_t);
/** Signature of memcpy */
typedef void *(*MEMCPY_T)(void *, const void *, rb_size_t); 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 { struct RingBuf {
rb_size_t struct_size; /** Size of the struct */ rb_size_t struct_size; /* Size of the struct */
rb_size_t capacity; /** The physical capacity of the entire ringbuf */ rb_size_t capacity; /* The physical capacity of the entire ringbuf */
rb_size_t count; /** The number of elements in the buffer */ rb_size_t count; /* The number of elements in the buffer */
void *write_head; /** Address of the write head */ void *write_head; /* Address of the write head */
void *read_head; /** Address of the read head */ void *read_head; /* Address of the read head */
void *buffer; /** The actual data */ void *buffer; /* The actual data */
void *buffer_end; /** The end of the buffer */ void *buffer_end; /* The end of the buffer */
} __attribute__((packed)); } __attribute__((packed));
/* // TODO: Perhaps unify these to RBResult?
* Considerations: One hot encoding for the enum values
* (8 bit each with -fshort-enums)
*/
/** Result of a write */ enum WriteResult { Full, WriteOk }; /** Result of a write */
enum WriteResult { enum ReadResult { Empty, ReadOk }; /** Result of a read */
WriteOk, /** The write was successful */
Full, /** The buffer is full */
InsufficientSpace /** There is not enough space to write */
};
/** Result of a read */ /** Initialize the ring buffer */
enum ReadResult { void rb_init(struct RingBuf *rb, rb_size_t capacity, void *(*alloc)(rb_size_t),
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); 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, enum WriteResult rb_push_back(struct RingBuf *rb, const void *item,
MEMCPY_T memcpy_fn); MEMCPY_T memcpy_fn);
/** /** Read data from the ring buffer */
* @brief Insert multiple data to the ring buffer enum ReadResult rb_pop_front(struct RingBuf *rb, void *item);
*
* @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)()); 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

View file

@ -1,101 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <cmocka.h>
#include <stdio.h>
#include <stdlib.h>
#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);
}