diff --git a/src/syncterm/queue/CMakeLists.txt b/src/syncterm/queue/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..1aabddaaa77c8c5aac172d2d944a08baa5b4fee7 --- /dev/null +++ b/src/syncterm/queue/CMakeLists.txt @@ -0,0 +1,25 @@ +# 3.21 added C23 +# 3.23 added file sets +cmake_minimum_required(VERSION 3.31) +project(llq VERSION 0.0 LANGUAGES C) + +add_library(llq) +set_property(TARGET llq PROPERTY C_STANDARD 23) +set_property(TARGET llq PROPERTY C_STANDARD_REQUIRED ON) +target_sources(llq + PRIVATE llq_alloc.c llq_dq.c llq_free.c llq_get_err.c llq_nq.c llq_was_mt.c + INTERFACE FILE_SET HEADERS FILES llq.h +) + +target_include_directories(llq INTERFACE .) +target_link_libraries(llq INTERFACE stdthreads) +target_compile_definitions(llq PRIVATE $<$<CONFIG:Release>:NDEBUG>) + +option(BUILD_TESTING "Perform unit tests after build" OFF) +if (BUILD_TESTING) + add_subdirectory(test) + enable_testing() + add_test(NAME llq_alloc COMMAND test_llq_alloc) +endif (BUILD_TESTING) + +install(TARGETS llq FILE_SET HEADERS) diff --git a/src/syncterm/queue/llq.h b/src/syncterm/queue/llq.h new file mode 100644 index 0000000000000000000000000000000000000000..1b046a05d91ec5f4b995b3572d1ccefbd81ef0cd --- /dev/null +++ b/src/syncterm/queue/llq.h @@ -0,0 +1,71 @@ +#ifndef DEUCE_QUEUE_H +#define DEUCE_QUEUE_H + +#include <stdalign.h> +#include <stdint.h> +#include <threads.h> + +typedef struct linked_list_queue *llq; +typedef intptr_t llq_err_t; + +enum llq_errval : llq_err_t { + llq_err_none, + llq_err_badflags, + llq_err_calloc, + llq_err_cnd_broadcast, + llq_err_cnd_init, + llq_err_cnd_signal, + llq_err_cnd_timedwait, + llq_err_cnd_wait, + llq_err_null_llq, + llq_err_mtx_destroy, + llq_err_mtx_init, + llq_err_mtx_lock, + llq_err_mtx_unlock, + llq_err_not_empty, + llq_err_timedout, + llq_err_tss_create, +}; + +/* + * Allocates a queue. flags must be zero. + * If flags is zero, the queue is FIFO + * If err is not nullptr, will be filled if llq == nullptr + */ +llq llq_alloc(llq_err_t *err, uint32_t flags, ...); + +/* + * Frees a queue allocated with llq_alloc() + * if err isn't nullptr, will be filled if an error occurs + * in the case of errors, llq_free() pretends there were no errors. + * Hopefully this will crash your program. You're welcome. + */ +void llq_free(llq restrict llq, llq_err_t *err); + +// Enqueues data in llq +bool llq_nq(llq restrict llq, void * restrict data); + +// Dequeues an enqueued value from llq +void * llq_dq(llq restrict llq); + +/* + * Blocks for ts time or until it can dequeue a value from llq. + * if ts is nullptr, blocks forever. + */ +void * llq_dq_wait(llq restrict llq, const struct timespec * restrict ts); + +/* + * Checks llq for an item. If there was an item in llq when + * checked, returns true. Returns false in all other cases + */ +bool llq_was_empty(llq restrict llq); + +/* + * Gets the error status of the queue. + * Must be called from the same thread where the error occured. + * msg is only valid until the next call to any llq_*() function. + * Returns true if no errors occured while getting the error. hehhehheh. + */ +bool llq_get_err(llq restrict llq, llq_err_t * restrict num, char8_t ** restrict msg); + +#endif diff --git a/src/syncterm/queue/llq_alloc.c b/src/syncterm/queue/llq_alloc.c new file mode 100644 index 0000000000000000000000000000000000000000..21d36c67a94da61d46c1f49e907ebe1a0981b948 --- /dev/null +++ b/src/syncterm/queue/llq_alloc.c @@ -0,0 +1,50 @@ +#include <stdlib.h> + +#include "llq_int.h" + +llq +llq_alloc(llq_err_t *err, uint32_t flags, ...) +{ + if (flags != 0) { + SET_ERR(err, llq_err_badflags); + return nullptr; + } + + llq ret = calloc(1, sizeof(struct linked_list_queue)); + if (ret == nullptr) { + SET_ERR(err, llq_err_calloc); + return nullptr; + } + ret->head = nullptr; + ret->tail = nullptr; + if (mtx_init(&ret->mtx, mtx_plain) != thrd_success) { + SET_ERR(err, llq_err_mtx_init); + free(ret); + return nullptr; + } + if (cnd_init(&ret->cnd) != thrd_success) { + SET_ERR(err, llq_err_cnd_init); + mtx_destroy(&ret->mtx); + free(ret); + return nullptr; + } + if (tss_create(&ret->errnum, nullptr) != thrd_success) { + SET_ERR(err, llq_err_tss_create); + cnd_destroy(&ret->cnd); + mtx_destroy(&ret->mtx); + free(ret); + return nullptr; + } + if (tss_create(&ret->errmsg, free) != thrd_success) { + SET_ERR(err, llq_err_tss_create); + tss_delete(ret->errnum); + cnd_destroy(&ret->cnd); + mtx_destroy(&ret->mtx); + free(ret); + return nullptr; + } + if (err) + *err = llq_err_none; + + return ret; +} diff --git a/src/syncterm/queue/llq_dq.c b/src/syncterm/queue/llq_dq.c new file mode 100644 index 0000000000000000000000000000000000000000..faf3d5ca61ff3d389d3a9aa83f24522d44683ef7 --- /dev/null +++ b/src/syncterm/queue/llq_dq.c @@ -0,0 +1,86 @@ +#include <stddef.h> +#include <stdlib.h> + +#include "llq_int.h" + +static void * +dq_locked(llq restrict llq) +{ + assert(llq != nullptr && "llq is nullptr"); + if (llq->head == nullptr) + return nullptr; + struct linked_list_queue_entry * restrict qe = llq->head; + llq->head = qe->next; + if (llq->tail == qe) + llq->tail = nullptr; + return qe; +} + +void * +llq_dq(llq restrict llq) +{ + void * restrict ret; + struct linked_list_queue_entry * restrict qe; + + CHECK_Q(llq, nullptr); + MTX_LOCK(llq, nullptr); + qe = dq_locked(llq); + MTX_UNLOCK(llq); + + if (qe == nullptr) + return nullptr; + ret = qe->data; + free(qe); + return ret; +} + +#define TRY_DQ(llq) do { \ + struct linked_list_queue_entry * restrict qe = dq_locked(llq); \ + if (qe != nullptr) { \ + MTX_UNLOCK(llq); \ + void * restrict ret = qe->data; \ + free(qe); \ + return ret; \ + } \ +} while(0) + +static void * +dq_wait_forever(llq restrict llq) +{ + for (;;) { + if (cnd_wait(&llq->cnd, &llq->mtx) != thrd_success) { + SET_ERROR(llq, llq_err_cnd_wait, "cnd_wait() failed"); + MTX_UNLOCK(llq); + return nullptr; + } + TRY_DQ(llq); + } + unreachable(); +} + +static void * +dq_wait_timed(llq restrict llq, const struct timespec * restrict ts) +{ + if (cnd_timedwait(&llq->cnd, &llq->mtx, ts) != thrd_success) { + SET_ERROR(llq, llq_err_cnd_timedwait, "cnd_timedwait() failed"); + MTX_UNLOCK(llq); + return nullptr; + } + + TRY_DQ(llq); + SET_ERROR(llq, llq_err_timedout, "timeout expired"); + MTX_UNLOCK(llq); + return nullptr; +} + +void * +llq_dq_wait(llq restrict llq, const struct timespec * restrict ts) +{ + CHECK_Q(llq, nullptr); + MTX_LOCK(llq, nullptr); + TRY_DQ(llq); + + if (ts == nullptr) + return dq_wait_forever(llq); + return dq_wait_timed(llq, ts); +} diff --git a/src/syncterm/queue/llq_free.c b/src/syncterm/queue/llq_free.c new file mode 100644 index 0000000000000000000000000000000000000000..29329b28d22697793ee6bfc38c4e9e406c0fcfc2 --- /dev/null +++ b/src/syncterm/queue/llq_free.c @@ -0,0 +1,32 @@ +#include <stdlib.h> + +#include "llq_int.h" + +void +llq_free(llq restrict llq, llq_err_t *err) +{ + if (llq == nullptr) { + SET_ERR(err, llq_err_null_llq); + return; + } + if (mtx_lock(&llq->mtx) != thrd_success) + SET_ERR(err, llq_err_mtx_lock); + for (struct linked_list_queue_entry * restrict qe = llq->head; qe; qe = llq->head) { + SET_ERR(err, llq_err_not_empty); + llq->head = qe->next; + free(qe); + } + // Try to make anyone else waiting crash... + // We could memset these things to make it more likely. 😈 + tss_delete(llq->errnum); + tss_delete(llq->errmsg); + if (cnd_broadcast(&llq->cnd) != thrd_success) + SET_ERR(err, llq_err_cnd_broadcast); + cnd_destroy(&llq->cnd); + if (mtx_unlock(&llq->mtx) != thrd_success) + SET_ERR(err, llq_err_mtx_unlock); + mtx_destroy(&llq->mtx); + if (err) + *err = llq_err_none; + free(llq); +} diff --git a/src/syncterm/queue/llq_get_err.c b/src/syncterm/queue/llq_get_err.c new file mode 100644 index 0000000000000000000000000000000000000000..9ec5ab7993b28ad375bfee9bfcda3ab8b5bbe914 --- /dev/null +++ b/src/syncterm/queue/llq_get_err.c @@ -0,0 +1,12 @@ +#include "llq_int.h" + +bool +llq_get_err(llq restrict llq, llq_err_t * restrict num, char8_t ** restrict msg) +{ + CHECK_Q(llq, false); + if (num != nullptr) + *num = (llq_err_t)tss_get(llq->errnum); + if (msg != nullptr) + *msg = (char8_t *)tss_get(llq->errnum); + return true; +} diff --git a/src/syncterm/queue/llq_int.h b/src/syncterm/queue/llq_int.h new file mode 100644 index 0000000000000000000000000000000000000000..e0305ecbbbebbab3a81c9534876e80ae6fbab6a6 --- /dev/null +++ b/src/syncterm/queue/llq_int.h @@ -0,0 +1,78 @@ +#ifndef DEUCE_QUEUE_INT_H +#define DEUCE_QUEUE_INT_H + +#include <assert.h> +#include <stdlib.h> +#include <string.h> + +// Needed to test asserts +#ifdef UNIT_TESTING +extern void mock_assert(const int result, const char* const expression, + const char * const file, const int line); + +#undef assert +#define assert(...) my_mock_assert((int)(__VA_ARGS__), #__VA_ARGS__, __FILE__, __LINE__); +#endif + +#include "llq.h" + +#ifndef CACHE_LINE_SIZE +#define CACHE_LINE_SIZE 64 +#endif + +#define SET_ERROR(llq, num, msg) do { \ + assert(!msg); \ + free(tss_get(llq->errmsg)); \ + char *amsg = strdup(msg); \ + tss_set(llq->errmsg, amsg); \ +} while(0) + +#define SET_ERR(err, val) do { \ + assert(!val); \ + if (err != nullptr) \ + *err = val; \ +} while(0) + +#define MTX_LOCK(llq, failval) do { \ + int err = mtx_lock(&llq->mtx); \ + if (err != thrd_success) { \ + assert(!"msg_lock failed!"); \ + SET_ERROR(llq, llq_err_mtx_lock, u8"failed to lock"); \ + /* TODO: Figure out debug log stuff... */ \ + return failval; \ + } \ +} while (0) + +#define MTX_UNLOCK(llq) do { \ + int err = mtx_unlock(&llq->mtx); \ + if (err != thrd_success) { \ + assert(!"mtx_unlock failed!"); \ + SET_ERROR(llq, llq_err_mtx_unlock, u8"failed to unlock"); \ + /* TODO: Figure out debug log stuff... */ \ + } \ +} while (0) + +#define CHECK_Q(llq, failval) do { \ + assert(llq != nullptr && "llq is null"); \ + if (llq == nullptr) { \ + SET_ERROR(llq, llq_err_null_llq, "llq is null"); \ + return failval; \ + } \ +} while(0) + +struct linked_list_queue_entry { + struct linked_list_queue_entry *next; + void *data; +}; + +struct linked_list_queue { + mtx_t mtx; + struct linked_list_queue_entry *head; + struct linked_list_queue_entry *tail; + tss_t errnum; + tss_t errmsg; + // TODO: Add a cache for allocated entries... + alignas((sizeof(mtx_t) + sizeof(struct linked_list_queue_entry *) * 2) > CACHE_LINE_SIZE ? 0 : CACHE_LINE_SIZE) cnd_t cnd; +}; + +#endif diff --git a/src/syncterm/queue/llq_nq.c b/src/syncterm/queue/llq_nq.c new file mode 100644 index 0000000000000000000000000000000000000000..99a786d8ca43bbac39132ba5c73234d5f779926d --- /dev/null +++ b/src/syncterm/queue/llq_nq.c @@ -0,0 +1,30 @@ +#include <stdlib.h> + +#include "llq_int.h" + +bool +llq_nq(llq restrict llq, void * restrict data) +{ + CHECK_Q(llq, false); + + struct linked_list_queue_entry * restrict qe = calloc(1, sizeof(struct linked_list_queue_entry)); + if (qe == nullptr) { + MTX_LOCK(llq, false); + SET_ERROR(llq, llq_err_calloc, "unable to calloc new entry"); + MTX_UNLOCK(llq); + return false; + } + qe->next = nullptr; + qe->data = data; + MTX_LOCK(llq, false); + if (llq->tail != nullptr) + llq->tail->next = qe; + if (llq->head == nullptr) { + llq->head = qe; + if (cnd_signal(&llq->cnd) != thrd_success) + SET_ERROR(llq, llq_err_cnd_signal, "cnd_signal() failed"); + } + llq->tail = qe; + MTX_UNLOCK(llq); + return true; +} diff --git a/src/syncterm/queue/llq_was_mt.c b/src/syncterm/queue/llq_was_mt.c new file mode 100644 index 0000000000000000000000000000000000000000..d98b7b0dca61b4ca141f8403bbf8633956bc4c17 --- /dev/null +++ b/src/syncterm/queue/llq_was_mt.c @@ -0,0 +1,16 @@ +#include <stdlib.h> + +#include "llq_int.h" + +bool +llq_was_mt(llq restrict llq) +{ + bool ret; + + CHECK_Q(llq, false); + MTX_LOCK(llq, nullptr); + ret = (llq->head == nullptr); + MTX_UNLOCK(llq); + + return ret; +} diff --git a/src/syncterm/queue/test/CMakeLists.txt b/src/syncterm/queue/test/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..523a0829d87d22870c9000b67d89edb3571ac7f8 --- /dev/null +++ b/src/syncterm/queue/test/CMakeLists.txt @@ -0,0 +1,12 @@ +# 3.21 added C23 +cmake_minimum_required(VERSION 3.31) +find_package(PkgConfig REQUIRED) + +pkg_check_modules(CMocka cmocka REQUIRED) + +add_executable(test_llq_alloc test_llq_alloc.c) +set_property(TARGET test_llq_alloc PROPERTY C_STANDARD 23) +target_compile_definitions(test_llq_alloc PRIVATE UNIT_TESTING) +target_include_directories(test_llq_alloc PRIVATE ${CMocka_INCLUDE_DIRS} ${llq_SOURCE_DIR}) +target_link_directories(test_llq_alloc PRIVATE ${CMocka_LIBRARY_DIRS}) +target_link_libraries(test_llq_alloc ${CMocka_LIBRARIES}) diff --git a/src/syncterm/queue/test/test_llq.c b/src/syncterm/queue/test/test_llq.c new file mode 100644 index 0000000000000000000000000000000000000000..59053863330725401714c3a778b03ed8200f66ad --- /dev/null +++ b/src/syncterm/queue/test/test_llq.c @@ -0,0 +1,20 @@ +#include <stdarg.h> +#include <stddef.h> +#include <stdint.h> +#include <setjmp.h> + +#include <cmocka.h> + +#include "llq.h" + +static llq llq; + + + +main(int argc, char **argv) +{ + const struct CMUnitTest tests[] = { + }; + + return cmocka_run_group_tests(tests, nullptr, nullptr); +} diff --git a/src/syncterm/queue/test/test_llq_alloc.c b/src/syncterm/queue/test/test_llq_alloc.c new file mode 100644 index 0000000000000000000000000000000000000000..0dcfb352d402dd098ac238718291f516eb705d60 --- /dev/null +++ b/src/syncterm/queue/test/test_llq_alloc.c @@ -0,0 +1,279 @@ +#include <errno.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdint.h> +#include <setjmp.h> +#include <stdlib.h> + +extern void _test_free(void* const ptr, const char* file, const int line); +void +test_free(void *ptr) { + _test_free(ptr, "Unknown", -1); +} + +#include <cmocka.h> + +#include "llq.h" + +int +mtx_init(mtx_t *mtx, int type) +{ + return mock(); +} + +void +mtx_destroy(mtx_t *mtx) +{ + function_called(); +} + +int +cnd_init(cnd_t *cnd) +{ + return mock(); +} + +void +cnd_destroy(cnd_t *cnd) +{ + function_called(); +} + +int +tss_create(tss_t *key, void (*dtor)(void *)) +{ + return mock(); +} + +void +tss_delete(tss_t key) +{ + function_called(); +} + +static int assert_count = -1; +static void +my_mock_assert(const int result, const char* const expression, + const char * const file, const int line) +{ + if (assert_count == -1) + fail(); + assert_count++; +} + +static int malloc_count = -1; +static void * +my_test_calloc(size_t number, const size_t size, const char *file, const int line) +{ + if (malloc_count == 0) { + errno = ENOMEM; + return nullptr; + } + if (malloc_count > 0) + malloc_count--; + return _test_calloc(number, size, file, line); +} + +void +test_setup() +{ + assert_count = -1; + malloc_count = -1; +} + +#define llq_test_err(atype, flags, err) \ + llq_err_t e; \ + void *ret; \ + e = (llq_err_t)&e; \ + assert_count = 0; \ + atype(ret = llq_alloc(&e, flags)); \ + free(ret); \ + assert_true(e == err) + +#define llq_test_noerr(atype, flags) \ + void *ret; \ + assert_count = 0; \ + ret = llq_alloc(nullptr, flags); \ + atype(ret); \ + free(ret); + +void +illegal_flags_error(void **state) +{ + test_setup(); + llq_test_err(assert_null, 1, llq_err_badflags); + assert_true(assert_count == 1); +} + +void +illegal_flags_noerror(void **state) +{ + test_setup(); + llq_test_noerr(assert_null, 1); + assert_true(assert_count == 1); +} + +void +calloc_failed_error(void **state) +{ + test_setup(); + malloc_count = 0; + llq_test_err(assert_null, 0, llq_err_calloc); + assert_true(assert_count == 1); +} + +void +calloc_failed_noerror(void **state) +{ + test_setup(); + malloc_count = 0; + llq_test_noerr(assert_null, 0); + assert_true(assert_count == 1); +} + +void +mtx_init_failed_error(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_error); + llq_test_err(assert_null, 0, llq_err_mtx_init); + assert_true(assert_count == 1); +} + +void +mtx_init_failed_noerror(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_error); + llq_test_noerr(assert_null, 0); + assert_true(assert_count == 1); +} + +void +cnd_init_failed_error(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_success); + will_return(cnd_init, thrd_error); + expect_function_call(mtx_destroy); + llq_test_err(assert_null, 0, llq_err_cnd_init); + assert_true(assert_count == 1); +} + +void +cnd_init_failed_noerror(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_success); + will_return(cnd_init, thrd_error); + expect_function_call(mtx_destroy); + llq_test_noerr(assert_null, 0); + assert_true(assert_count == 1); +} + +void +tss_create_failed_error(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_success); + will_return(cnd_init, thrd_success); + will_return(tss_create, thrd_error); + expect_function_call(cnd_destroy); + expect_function_call(mtx_destroy); + llq_test_err(assert_null, 0, llq_err_tss_create); + assert_true(assert_count == 1); +} + +void +tss_create_failed_noerror(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_success); + will_return(cnd_init, thrd_success); + will_return(tss_create, thrd_error); + expect_function_call(cnd_destroy); + expect_function_call(mtx_destroy); + llq_test_noerr(assert_null, 0); + assert_true(assert_count == 1); +} + +void +tss_create_failed_error2(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_success); + will_return(cnd_init, thrd_success); + will_return(tss_create, thrd_success); + will_return(tss_create, thrd_error); + expect_function_call(tss_delete); + expect_function_call(cnd_destroy); + expect_function_call(mtx_destroy); + llq_test_err(assert_null, 0, llq_err_tss_create); + assert_true(assert_count == 1); +} + +void +tss_create_failed_noerror2(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_success); + will_return(cnd_init, thrd_success); + will_return(tss_create, thrd_success); + will_return(tss_create, thrd_error); + expect_function_call(tss_delete); + expect_function_call(cnd_destroy); + expect_function_call(mtx_destroy); + llq_test_noerr(assert_null, 0); + assert_true(assert_count == 1); +} + +void +success_error(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_success); + will_return(cnd_init, thrd_success); + will_return(tss_create, thrd_success); + will_return(tss_create, thrd_success); + llq_test_err(assert_non_null, 0, llq_err_none); + assert_true(assert_count == 0); +} + +void +success_noerror(void **state) +{ + test_setup(); + will_return(mtx_init, thrd_success); + will_return(cnd_init, thrd_success); + will_return(tss_create, thrd_success); + will_return(tss_create, thrd_success); + llq_test_noerr(assert_non_null, 0); + assert_true(assert_count == 0); +} + +int +main(int argc, char **argv) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(illegal_flags_error), + cmocka_unit_test(illegal_flags_noerror), + cmocka_unit_test(calloc_failed_error), + cmocka_unit_test(calloc_failed_noerror), + cmocka_unit_test(mtx_init_failed_error), + cmocka_unit_test(mtx_init_failed_noerror), + cmocka_unit_test(cnd_init_failed_error), + cmocka_unit_test(cnd_init_failed_noerror), + cmocka_unit_test(tss_create_failed_error), + cmocka_unit_test(tss_create_failed_noerror), + cmocka_unit_test(tss_create_failed_error2), + cmocka_unit_test(tss_create_failed_noerror2), + cmocka_unit_test(success_error), + cmocka_unit_test(success_noerror), + }; + + return cmocka_run_group_tests(tests, nullptr, nullptr); +} + +#undef test_calloc +#define test_calloc(number, size) my_test_calloc(number, size, __FILE__, __LINE__) +#include "../llq_alloc.c"