From a9cd3e4e380fcd14057c1449097f173e08165f8a Mon Sep 17 00:00:00 2001 From: Andrey Zheltovskiy Date: Mon, 27 Apr 2026 10:53:45 +0200 Subject: [PATCH 1/6] Add platform-specific monotonic time utilities Introduce kit::time with steady_now_ns() for high-resolution monotonic timestamps on Linux (clock_gettime), macOS (mach_absolute_time), and Windows (QueryPerformanceCounter). Made-with: Cursor --- include/kit/time.hpp | 14 ++++++++++++++ src/CMakeLists.txt | 6 +++--- src/time/time_linux.cpp | 22 ++++++++++++++++++++++ src/time/time_macos.cpp | 21 +++++++++++++++++++++ src/time/time_win32.cpp | 27 +++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 include/kit/time.hpp create mode 100644 src/time/time_linux.cpp create mode 100644 src/time/time_macos.cpp create mode 100644 src/time/time_win32.cpp diff --git a/include/kit/time.hpp b/include/kit/time.hpp new file mode 100644 index 0000000..f366432 --- /dev/null +++ b/include/kit/time.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace kit +{ + +/// Returns the current value of a high-resolution monotonic tick counter. +uint64_t get_hires_ticks(); + +/// Returns the tick-to-nanosecond conversion ratio: nanoseconds = ticks * o_numer / o_denom. +void get_hires_ticks_freq(uint64_t &o_numer, uint64_t &o_denom); + +} // namespace kit diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ff1ea65..32321fb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,11 +3,11 @@ set(KIT_SOURCES ) if(KIT_PLATFORM_LINUX) - list(APPEND KIT_SOURCES shared/shared_linux.cpp) + list(APPEND KIT_SOURCES shared/shared_linux.cpp time/time_linux.cpp) elseif(KIT_PLATFORM_DARWIN) - list(APPEND KIT_SOURCES shared/shared_macos.cpp) + list(APPEND KIT_SOURCES shared/shared_macos.cpp time/time_macos.cpp) elseif(KIT_PLATFORM_WINDOWS) - list(APPEND KIT_SOURCES shared/shared_win32.cpp) + list(APPEND KIT_SOURCES shared/shared_win32.cpp time/time_win32.cpp) endif() add_library(kit_obj OBJECT ${KIT_SOURCES}) diff --git a/src/time/time_linux.cpp b/src/time/time_linux.cpp new file mode 100644 index 0000000..33b4c8d --- /dev/null +++ b/src/time/time_linux.cpp @@ -0,0 +1,22 @@ +#include "kit/time.hpp" + +#include + +namespace kit +{ + +uint64_t get_hires_ticks() +{ + struct timespec l_sTs; + clock_gettime(CLOCK_MONOTONIC, &l_sTs); + return static_cast(l_sTs.tv_sec) * 1000000000ULL + static_cast(l_sTs.tv_nsec); +} + +void get_hires_ticks_freq(uint64_t &o_numer, uint64_t &o_denom) +{ + // clock_gettime already returns nanoseconds, ratio is 1:1 + o_numer = 1; + o_denom = 1; +} + +} // namespace kit diff --git a/src/time/time_macos.cpp b/src/time/time_macos.cpp new file mode 100644 index 0000000..c3d21d8 --- /dev/null +++ b/src/time/time_macos.cpp @@ -0,0 +1,21 @@ +#include "kit/time.hpp" + +#include + +namespace kit +{ + +uint64_t get_hires_ticks() +{ + return mach_absolute_time(); +} + +void get_hires_ticks_freq(uint64_t &o_numer, uint64_t &o_denom) +{ + mach_timebase_info_data_t l_sInfo; + mach_timebase_info(&l_sInfo); + o_numer = l_sInfo.numer; + o_denom = l_sInfo.denom; +} + +} // namespace kit diff --git a/src/time/time_win32.cpp b/src/time/time_win32.cpp new file mode 100644 index 0000000..99970df --- /dev/null +++ b/src/time/time_win32.cpp @@ -0,0 +1,27 @@ +#include "kit/time.hpp" + +#if !defined(_WINSOCKAPI_) + #include +#endif +#include + +namespace kit +{ + +uint64_t get_hires_ticks() +{ + LARGE_INTEGER l_sCounter; + QueryPerformanceCounter(&l_sCounter); + return static_cast(l_sCounter.QuadPart); +} + +void get_hires_ticks_freq(uint64_t &o_numer, uint64_t &o_denom) +{ + LARGE_INTEGER l_sFreq; + QueryPerformanceFrequency(&l_sFreq); + // ticks * 1e9 / freq = nanoseconds, so numer=1e9, denom=freq + o_numer = 1000000000ULL; + o_denom = static_cast(l_sFreq.QuadPart); +} + +} // namespace kit From ca097cb64e704948351f107a7acc5b5bca3e26ec Mon Sep 17 00:00:00 2001 From: Andrey Zheltovskiy Date: Mon, 27 Apr 2026 10:53:50 +0200 Subject: [PATCH 2/6] Enable compile_commands.json generation for clangd support Add CMAKE_EXPORT_COMPILE_COMMANDS to macOS and Linux presets so clangd can provide accurate diagnostics and navigation. Made-with: Cursor --- .gitignore | 4 ++++ CMakePresets.json | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f902153..6a13ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,7 @@ CMakeUserPresets.json .DS_Store scripts/__pycache__/** + +# Generated compile database (symlink to build dir) +compile_commands.json +.cache/ diff --git a/CMakePresets.json b/CMakePresets.json index b05ce23..4ae16a4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -15,7 +15,8 @@ "cacheVariables": { "KIT_BUILD_SCRIPT": "ON", "CMAKE_BUILD_TYPE": "Release", - "CMAKE_OSX_SYSROOT": "macosx" + "CMAKE_OSX_SYSROOT": "macosx", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } }, { @@ -23,7 +24,8 @@ "binaryDir": "${sourceDir}/_Build_lnx", "cacheVariables": { "KIT_BUILD_SCRIPT": "ON", - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } } ] From 6851404ba96df363ddcb9c31afb9042baf7826d6 Mon Sep 17 00:00:00 2001 From: Andrey Zheltovskiy Date: Mon, 27 Apr 2026 22:23:47 +0200 Subject: [PATCH 3/6] feat(system): add platform-specific process and host info utilities Provides get_process_id(), get_process_name(), get_host_name() across macOS, Linux, and Windows to centralise system info queries in kit. Made-with: Cursor --- include/kit/system.hpp | 20 ++++++++++++ src/CMakeLists.txt | 6 ++-- src/system/system_linux.cpp | 65 +++++++++++++++++++++++++++++++++++++ src/system/system_macos.cpp | 56 ++++++++++++++++++++++++++++++++ src/system/system_win32.cpp | 65 +++++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 include/kit/system.hpp create mode 100644 src/system/system_linux.cpp create mode 100644 src/system/system_macos.cpp create mode 100644 src/system/system_win32.cpp diff --git a/include/kit/system.hpp b/include/kit/system.hpp new file mode 100644 index 0000000..fa5e30f --- /dev/null +++ b/include/kit/system.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace kit +{ + +/// Returns the current process ID. +uint32_t get_process_id(); + +/// Fills op_buf with the executable name (without path). Null-terminates. +/// @return true on success, false on failure (op_buf set to "unknown"). +bool get_process_name(char *op_buf, size_t iz_buf); + +/// Fills op_buf with the host name. Null-terminates. +/// @return true on success, false on failure (op_buf set to "unknown"). +bool get_host_name(char *op_buf, size_t iz_buf); + +} // namespace kit diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 32321fb..a6fa65b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,11 +3,11 @@ set(KIT_SOURCES ) if(KIT_PLATFORM_LINUX) - list(APPEND KIT_SOURCES shared/shared_linux.cpp time/time_linux.cpp) + list(APPEND KIT_SOURCES shared/shared_linux.cpp time/time_linux.cpp system/system_linux.cpp) elseif(KIT_PLATFORM_DARWIN) - list(APPEND KIT_SOURCES shared/shared_macos.cpp time/time_macos.cpp) + list(APPEND KIT_SOURCES shared/shared_macos.cpp time/time_macos.cpp system/system_macos.cpp) elseif(KIT_PLATFORM_WINDOWS) - list(APPEND KIT_SOURCES shared/shared_win32.cpp time/time_win32.cpp) + list(APPEND KIT_SOURCES shared/shared_win32.cpp time/time_win32.cpp system/system_win32.cpp) endif() add_library(kit_obj OBJECT ${KIT_SOURCES}) diff --git a/src/system/system_linux.cpp b/src/system/system_linux.cpp new file mode 100644 index 0000000..fbc4978 --- /dev/null +++ b/src/system/system_linux.cpp @@ -0,0 +1,65 @@ +#include "kit/system.hpp" + +#include +#include + +namespace kit +{ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint32_t get_process_id() +{ + return static_cast(getpid()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool get_process_name(char *op_buf, size_t iz_buf) +{ + if(!op_buf || iz_buf < 2) + { + return false; + } + + char la_path[4096]; + int li_len = static_cast(readlink("/proc/self/exe", la_path, sizeof(la_path) - 1)); + + if(li_len <= 0) + { + std::strncpy(op_buf, "unknown", iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return false; + } + + la_path[li_len] = '\0'; + + const char *lp_name = la_path + li_len; + while(lp_name > la_path && *(lp_name - 1) != '/') + { + --lp_name; + } + + std::strncpy(op_buf, lp_name, iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool get_host_name(char *op_buf, size_t iz_buf) +{ + if(!op_buf || iz_buf < 2) + { + return false; + } + + if(gethostname(op_buf, iz_buf) == 0) + { + op_buf[iz_buf - 1] = '\0'; + return true; + } + + std::strncpy(op_buf, "unknown", iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return false; +} + +} // namespace kit diff --git a/src/system/system_macos.cpp b/src/system/system_macos.cpp new file mode 100644 index 0000000..0d665e8 --- /dev/null +++ b/src/system/system_macos.cpp @@ -0,0 +1,56 @@ +#include "kit/system.hpp" + +#include +#include +#include + +namespace kit +{ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint32_t get_process_id() +{ + return static_cast(getpid()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool get_process_name(char *op_buf, size_t iz_buf) +{ + if(!op_buf || iz_buf < 2) + { + return false; + } + + const char *lp_name = getprogname(); + if(lp_name) + { + std::strncpy(op_buf, lp_name, iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return true; + } + + std::strncpy(op_buf, "unknown", iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool get_host_name(char *op_buf, size_t iz_buf) +{ + if(!op_buf || iz_buf < 2) + { + return false; + } + + if(gethostname(op_buf, iz_buf) == 0) + { + op_buf[iz_buf - 1] = '\0'; + return true; + } + + std::strncpy(op_buf, "unknown", iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return false; +} + +} // namespace kit diff --git a/src/system/system_win32.cpp b/src/system/system_win32.cpp new file mode 100644 index 0000000..016b92d --- /dev/null +++ b/src/system/system_win32.cpp @@ -0,0 +1,65 @@ +#include "kit/system.hpp" + +#include +#include + +namespace kit +{ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +uint32_t get_process_id() +{ + return static_cast(GetCurrentProcessId()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool get_process_name(char *op_buf, size_t iz_buf) +{ + if(!op_buf || iz_buf < 2) + { + return false; + } + + char la_path[MAX_PATH]; + DWORD lu_len = GetModuleFileNameA(nullptr, la_path, MAX_PATH); + + if(lu_len == 0) + { + std::strncpy(op_buf, "unknown", iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return false; + } + + la_path[MAX_PATH - 1] = '\0'; + + const char *lp_name = la_path + lu_len; + while(lp_name > la_path && *(lp_name - 1) != '\\' && *(lp_name - 1) != '/') + { + --lp_name; + } + + std::strncpy(op_buf, lp_name, iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool get_host_name(char *op_buf, size_t iz_buf) +{ + if(!op_buf || iz_buf < 2) + { + return false; + } + + DWORD lu_size = static_cast(iz_buf); + if(GetComputerNameA(op_buf, &lu_size)) + { + return true; + } + + std::strncpy(op_buf, "unknown", iz_buf - 1); + op_buf[iz_buf - 1] = '\0'; + return false; +} + +} // namespace kit From 14a078946c152d6c96b9cdc2e32820c9440760b0 Mon Sep 17 00:00:00 2001 From: Andrey Zheltovskiy Date: Mon, 27 Apr 2026 22:23:52 +0200 Subject: [PATCH 4/6] feat(endian): add portable byte-swap utilities Header-only bswap16/32/64 wrapping compiler intrinsics for GCC, Clang, and MSVC to support little-endian wire format encoding. Made-with: Cursor --- include/kit/endian.hpp | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 include/kit/endian.hpp diff --git a/include/kit/endian.hpp b/include/kit/endian.hpp new file mode 100644 index 0000000..3ba9999 --- /dev/null +++ b/include/kit/endian.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#if defined(_MSC_VER) + #include +#endif + +namespace kit +{ + +inline uint16_t bswap16(uint16_t iu_val) +{ +#if defined(_MSC_VER) + return _byteswap_ushort(iu_val); +#else + return __builtin_bswap16(iu_val); +#endif +} + +inline uint32_t bswap32(uint32_t iu_val) +{ +#if defined(_MSC_VER) + return _byteswap_ulong(iu_val); +#else + return __builtin_bswap32(iu_val); +#endif +} + +inline uint64_t bswap64(uint64_t iu_val) +{ +#if defined(_MSC_VER) + return _byteswap_uint64(iu_val); +#else + return __builtin_bswap64(iu_val); +#endif +} + +} // namespace kit From 14ffe811c027dd25b414f01022da154715c44080 Mon Sep 17 00:00:00 2001 From: Andrey Zheltovskiy Date: Mon, 27 Apr 2026 22:23:58 +0200 Subject: [PATCH 5/6] feat(time): add get_system_time() and get_utc_offset_seconds() get_system_time() returns 100ns intervals since 1601-01-01 UTC using GetSystemTimePreciseAsFileTime on Windows and clock_gettime(CLOCK_REALTIME) on POSIX. get_utc_offset_seconds() returns the local UTC offset. Made-with: Cursor --- include/kit/time.hpp | 6 ++++++ src/time/time_linux.cpp | 21 ++++++++++++++++++++- src/time/time_macos.cpp | 21 +++++++++++++++++++++ src/time/time_win32.cpp | 15 ++++++++++++++- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/include/kit/time.hpp b/include/kit/time.hpp index f366432..5a4a5c8 100644 --- a/include/kit/time.hpp +++ b/include/kit/time.hpp @@ -11,4 +11,10 @@ uint64_t get_hires_ticks(); /// Returns the tick-to-nanosecond conversion ratio: nanoseconds = ticks * o_numer / o_denom. void get_hires_ticks_freq(uint64_t &o_numer, uint64_t &o_denom); +/// Returns system time as 100-nanosecond intervals since January 1, 1601 (UTC). +uint64_t get_system_time(); + +/// Returns the local UTC offset in seconds (e.g. +3600 for UTC+1, -18000 for UTC-5). +int32_t get_utc_offset_seconds(); + } // namespace kit diff --git a/src/time/time_linux.cpp b/src/time/time_linux.cpp index 33b4c8d..bcc7e33 100644 --- a/src/time/time_linux.cpp +++ b/src/time/time_linux.cpp @@ -2,6 +2,9 @@ #include +static constexpr uint64_t gc_offset_1601_1970 = 116444736000000000ULL; +static constexpr uint64_t gc_100ns_per_sec = 10000000ULL; + namespace kit { @@ -14,9 +17,25 @@ uint64_t get_hires_ticks() void get_hires_ticks_freq(uint64_t &o_numer, uint64_t &o_denom) { - // clock_gettime already returns nanoseconds, ratio is 1:1 o_numer = 1; o_denom = 1; } +uint64_t get_system_time() +{ + struct timespec l_sTs; + clock_gettime(CLOCK_REALTIME, &l_sTs); + uint64_t lu_100ns + = static_cast(l_sTs.tv_sec) * gc_100ns_per_sec + static_cast(l_sTs.tv_nsec) / 100ULL; + return lu_100ns + gc_offset_1601_1970; +} + +int32_t get_utc_offset_seconds() +{ + time_t li_time = time(nullptr); + struct tm lo_local = {}; + localtime_r(&li_time, &lo_local); + return static_cast(lo_local.tm_gmtoff); +} + } // namespace kit diff --git a/src/time/time_macos.cpp b/src/time/time_macos.cpp index c3d21d8..671a73d 100644 --- a/src/time/time_macos.cpp +++ b/src/time/time_macos.cpp @@ -1,6 +1,10 @@ #include "kit/time.hpp" #include +#include + +static constexpr uint64_t gc_offset_1601_1970 = 116444736000000000ULL; +static constexpr uint64_t gc_100ns_per_sec = 10000000ULL; namespace kit { @@ -18,4 +22,21 @@ void get_hires_ticks_freq(uint64_t &o_numer, uint64_t &o_denom) o_denom = l_sInfo.denom; } +uint64_t get_system_time() +{ + struct timespec l_sTs; + clock_gettime(CLOCK_REALTIME, &l_sTs); + uint64_t lu_100ns + = static_cast(l_sTs.tv_sec) * gc_100ns_per_sec + static_cast(l_sTs.tv_nsec) / 100ULL; + return lu_100ns + gc_offset_1601_1970; +} + +int32_t get_utc_offset_seconds() +{ + time_t li_time = time(nullptr); + struct tm lo_local = {}; + localtime_r(&li_time, &lo_local); + return static_cast(lo_local.tm_gmtoff); +} + } // namespace kit diff --git a/src/time/time_win32.cpp b/src/time/time_win32.cpp index 99970df..9b1b205 100644 --- a/src/time/time_win32.cpp +++ b/src/time/time_win32.cpp @@ -19,9 +19,22 @@ void get_hires_ticks_freq(uint64_t &o_numer, uint64_t &o_denom) { LARGE_INTEGER l_sFreq; QueryPerformanceFrequency(&l_sFreq); - // ticks * 1e9 / freq = nanoseconds, so numer=1e9, denom=freq o_numer = 1000000000ULL; o_denom = static_cast(l_sFreq.QuadPart); } +uint64_t get_system_time() +{ + FILETIME l_sFt; + GetSystemTimePreciseAsFileTime(&l_sFt); + return (static_cast(l_sFt.dwHighDateTime) << 32) | static_cast(l_sFt.dwLowDateTime); +} + +int32_t get_utc_offset_seconds() +{ + TIME_ZONE_INFORMATION l_sTz = {}; + GetTimeZoneInformation(&l_sTz); + return static_cast(l_sTz.Bias) * -60; +} + } // namespace kit From ee8596fbc2f2d47fdfe0872241495e06e635786f Mon Sep 17 00:00:00 2001 From: Andrey Zheltovskiy Date: Wed, 24 Jun 2026 21:02:57 +0200 Subject: [PATCH 6/6] feat(event): add c_event cross-platform multi-event wait primitive - Windows: native WaitForMultipleObjects - Linux: POSIX unnamed semaphore + pthread mutex (original design) - macOS: GCD dispatch_semaphore (no unnamed POSIX semaphores there), clock-safe relative timeouts mirroring the Linux algorithm --- include/kit/event.hpp | 45 ++++++ include/kit/kit.h | 1 + src/CMakeLists.txt | 6 +- src/event/event_linux.cpp | 309 ++++++++++++++++++++++++++++++++++++++ src/event/event_macos.cpp | 299 ++++++++++++++++++++++++++++++++++++ src/event/event_win32.cpp | 189 +++++++++++++++++++++++ tests/event_test.hpp | 227 ++++++++++++++++++++++++++++ tests/main.cpp | 1 + 8 files changed, 1074 insertions(+), 3 deletions(-) create mode 100644 include/kit/event.hpp create mode 100644 src/event/event_linux.cpp create mode 100644 src/event/event_macos.cpp create mode 100644 src/event/event_win32.cpp create mode 100644 tests/event_test.hpp diff --git a/include/kit/event.hpp b/include/kit/event.hpp new file mode 100644 index 0000000..e38b9c5 --- /dev/null +++ b/include/kit/event.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +//////////////////////////////////////////////////////////////////////////////// +// c_event - multi-event wait primitive (wait-any). Manages a small set of +// events, each of type e_single_auto, e_single_manual or e_multi, +// and lets a thread wait until ANY of them is signaled, returning +// the index of the signaled event. +class c_event +{ +public: + // Opaque, platform-specific implementation (defined in each event_*.cpp). + struct s_impl; + + enum e_type + { + e_single_auto = 0, // auto-reset, releases one waiter per set() + e_single_manual, // manual-reset, stays signaled until clr() + e_multi // counting / semaphore + }; + + // Returned by wait() on timeout; valid signal indices are 0..(count - 1). + static constexpr uint32_t timeout = 0xFFFFFFFFu; + + c_event(); + ~c_event(); + + // Array-based replacement for the original va_list Init(count, ...). + bool init(uint8_t iu_count, const e_type *ip_types); + + bool set(uint32_t iu_id); + + bool clr(uint32_t iu_id); // only meaningful for e_single_manual + + uint32_t wait(); + + uint32_t wait(uint32_t iu_timeout_ms); + +private: + c_event(const c_event &) = delete; + c_event &operator=(const c_event &) = delete; + + s_impl *mp_impl; +}; diff --git a/include/kit/kit.h b/include/kit/kit.h index c26c789..13f119f 100644 --- a/include/kit/kit.h +++ b/include/kit/kit.h @@ -7,6 +7,7 @@ #include "kit/list.hpp" #include "kit/spin_lock.hpp" #include "kit/shared_mem.hpp" +#include "kit/event.hpp" #ifdef __cplusplus extern "C" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a6fa65b..27ed301 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,11 +3,11 @@ set(KIT_SOURCES ) if(KIT_PLATFORM_LINUX) - list(APPEND KIT_SOURCES shared/shared_linux.cpp time/time_linux.cpp system/system_linux.cpp) + list(APPEND KIT_SOURCES shared/shared_linux.cpp time/time_linux.cpp system/system_linux.cpp event/event_linux.cpp) elseif(KIT_PLATFORM_DARWIN) - list(APPEND KIT_SOURCES shared/shared_macos.cpp time/time_macos.cpp system/system_macos.cpp) + list(APPEND KIT_SOURCES shared/shared_macos.cpp time/time_macos.cpp system/system_macos.cpp event/event_macos.cpp) elseif(KIT_PLATFORM_WINDOWS) - list(APPEND KIT_SOURCES shared/shared_win32.cpp time/time_win32.cpp system/system_win32.cpp) + list(APPEND KIT_SOURCES shared/shared_win32.cpp time/time_win32.cpp system/system_win32.cpp event/event_win32.cpp) endif() add_library(kit_obj OBJECT ${KIT_SOURCES}) diff --git a/src/event/event_linux.cpp b/src/event/event_linux.cpp new file mode 100644 index 0000000..0d8e1d3 --- /dev/null +++ b/src/event/event_linux.cpp @@ -0,0 +1,309 @@ +#include "kit/event.hpp" + +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +// CMEvent design: a single counting semaphore tracks the total number of +// pending signals across all events, while per-event counters + a round-robin +// scan decide which event index a wait() returns. This avoids the abs.-time +// "wait" POSIX functions that are affected by system time changes. +struct c_event::s_impl +{ + struct s_event + { + int32_t mi_counter; + e_type me_type; + uint32_t mu_idx; + s_event *mp_next; + }; + + pthread_mutex_t ms_mutex; + sem_t ms_sem; + uint8_t mu_count; + bool mb_init; + bool mb_error; + s_event *mp_events; + s_event *mp_event_cur; + int32_t mi_signals; +}; + +//////////////////////////////////////////////////////////////////////////////// +// helpers +namespace +{ + +void cleanup(c_event::s_impl *ip_impl) +{ + if((nullptr != ip_impl) && (ip_impl->mp_events)) + { + delete[] ip_impl->mp_events; + ip_impl->mp_events = nullptr; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// walk through all events to find a pending signal, returns its index or +// c_event::timeout. Must be called under the mutex. +uint32_t get_signal(c_event::s_impl *ip_impl) +{ + uint32_t lu_return = c_event::timeout; + + if(0 == ip_impl->mi_signals) + { + return lu_return; + } + + c_event::s_impl::s_event *lp_start = ip_impl->mp_event_cur; + do + { + ip_impl->mp_event_cur = ip_impl->mp_event_cur->mp_next; + + if(ip_impl->mp_event_cur->mi_counter) + { + lu_return = ip_impl->mp_event_cur->mu_idx; + if(c_event::e_single_manual == ip_impl->mp_event_cur->me_type) + { + sem_post(&ip_impl->ms_sem); // keep manual events level-triggered + } + else + { + ip_impl->mp_event_cur->mi_counter--; + ip_impl->mi_signals--; + } + + break; + } + } while(lp_start != ip_impl->mp_event_cur); + + if(c_event::timeout == lu_return) + { + ip_impl->mi_signals--; + } + + return lu_return; +} + +} // anonymous namespace + +//////////////////////////////////////////////////////////////////////////////// +// c_event::c_event +c_event::c_event() + : mp_impl(new s_impl) +{ + mp_impl->mu_count = 0; + mp_impl->mb_init = false; + mp_impl->mb_error = false; + mp_impl->mp_events = nullptr; + mp_impl->mp_event_cur = nullptr; + mp_impl->mi_signals = 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::~c_event +c_event::~c_event() +{ + if(mp_impl->mb_init) + { + pthread_mutex_destroy(&mp_impl->ms_mutex); + sem_destroy(&mp_impl->ms_sem); + } + + cleanup(mp_impl); + + delete mp_impl; + mp_impl = nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::init +bool c_event::init(uint8_t iu_count, const e_type *ip_types) +{ + bool lb_sig = false; + bool lb_mutex = false; + uint8_t lu_idx = 0; + + if((true == mp_impl->mb_init) || (true == mp_impl->mb_error) || (0 >= iu_count) || (nullptr == ip_types)) + { + return false; + } + + //////////////////////////////////////////////////////////////////////////// + // initialize semaphore + if(0 != sem_init(&mp_impl->ms_sem, 0, 0)) + { + goto l_lblExit; + } + else + { + lb_sig = true; + } + + //////////////////////////////////////////////////////////////////////////// + // initialize mutex + if(0 != pthread_mutex_init(&mp_impl->ms_mutex, nullptr)) + { + mp_impl->mb_error = true; + goto l_lblExit; + } + else + { + lb_mutex = true; + } + + //////////////////////////////////////////////////////////////////////////// + // initialize events structure + mp_impl->mp_events = new s_impl::s_event[iu_count]; + if(nullptr == mp_impl->mp_events) + { + goto l_lblExit; + } + + memset(mp_impl->mp_events, 0, sizeof(s_impl::s_event) * iu_count); + + mp_impl->mp_event_cur = &mp_impl->mp_events[0]; + + while(lu_idx < iu_count) + { + mp_impl->mp_events[lu_idx].me_type = ip_types[lu_idx]; + mp_impl->mp_events[lu_idx].mi_counter = 0; + mp_impl->mp_events[lu_idx].mu_idx = lu_idx; + + // make round trip links + if((lu_idx + 1) < iu_count) + { + mp_impl->mp_events[lu_idx].mp_next = &mp_impl->mp_events[lu_idx + 1]; + } + else + { + mp_impl->mp_events[lu_idx].mp_next = &mp_impl->mp_events[0]; + } + + lu_idx++; + } + + mp_impl->mb_init = true; + mp_impl->mu_count = iu_count; + +l_lblExit: + if(false == mp_impl->mb_init) + { + mp_impl->mb_error = true; + + if(lb_mutex) + { + pthread_mutex_destroy(&mp_impl->ms_mutex); + } + + if(lb_sig) + { + sem_destroy(&mp_impl->ms_sem); + } + + cleanup(mp_impl); + } + + return mp_impl->mb_init; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::set +bool c_event::set(uint32_t iu_id) +{ + if((iu_id >= mp_impl->mu_count) || (false == mp_impl->mb_init)) + { + return false; + } + + pthread_mutex_lock(&mp_impl->ms_mutex); + + mp_impl->mi_signals++; + mp_impl->mp_events[iu_id].mi_counter++; + + sem_post(&mp_impl->ms_sem); + + pthread_mutex_unlock(&mp_impl->ms_mutex); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::clr +bool c_event::clr(uint32_t iu_id) +{ + bool lb_return = false; + + if((iu_id >= mp_impl->mu_count) || (e_single_manual != mp_impl->mp_events[iu_id].me_type)) + { + return lb_return; + } + + pthread_mutex_lock(&mp_impl->ms_mutex); + + if(mp_impl->mp_events[iu_id].mi_counter) + { + mp_impl->mi_signals--; + mp_impl->mp_events[iu_id].mi_counter--; + + sem_trywait(&mp_impl->ms_sem); // decrease semaphore + + lb_return = true; + } + + pthread_mutex_unlock(&mp_impl->ms_mutex); + + return lb_return; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::wait +uint32_t c_event::wait() +{ + uint32_t lu_return = c_event::timeout; + + sem_wait(&mp_impl->ms_sem); + + pthread_mutex_lock(&mp_impl->ms_mutex); + lu_return = get_signal(mp_impl); + pthread_mutex_unlock(&mp_impl->ms_mutex); + + return lu_return; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::wait +uint32_t c_event::wait(uint32_t iu_timeout_ms) +{ + uint64_t lu_nano = (uint64_t)iu_timeout_ms * 1000000ULL; + uint32_t lu_return = c_event::timeout; + struct timespec ls_time = { 0, 0 }; + + if(0 == iu_timeout_ms) + { + if(0 == sem_trywait(&mp_impl->ms_sem)) + { + pthread_mutex_lock(&mp_impl->ms_mutex); + lu_return = get_signal(mp_impl); + pthread_mutex_unlock(&mp_impl->ms_mutex); + } + } + else + { + clock_gettime(CLOCK_REALTIME, &ls_time); + + lu_nano += ls_time.tv_nsec; + ls_time.tv_sec += (time_t)(lu_nano / 1000000000ULL); + ls_time.tv_nsec = (long)(lu_nano % 1000000000ULL); + + if(0 == sem_timedwait(&mp_impl->ms_sem, &ls_time)) + { + pthread_mutex_lock(&mp_impl->ms_mutex); + lu_return = get_signal(mp_impl); + pthread_mutex_unlock(&mp_impl->ms_mutex); + } + } + + return lu_return; +} diff --git a/src/event/event_macos.cpp b/src/event/event_macos.cpp new file mode 100644 index 0000000..e7d239e --- /dev/null +++ b/src/event/event_macos.cpp @@ -0,0 +1,299 @@ +#include "kit/event.hpp" + +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +// Same design as the Linux CMEvent port, but the counting semaphore is a GCD +// dispatch_semaphore: macOS does not support unnamed POSIX semaphores +// (sem_init / sem_timedwait). dispatch_semaphore timeouts are relative and +// based on a monotonic clock, so they are immune to system time changes. +struct c_event::s_impl +{ + struct s_event + { + int32_t mi_counter; + e_type me_type; + uint32_t mu_idx; + s_event *mp_next; + }; + + pthread_mutex_t ms_mutex; + dispatch_semaphore_t mh_sem; + uint8_t mu_count; + bool mb_init; + bool mb_error; + s_event *mp_events; + s_event *mp_event_cur; + int32_t mi_signals; +}; + +//////////////////////////////////////////////////////////////////////////////// +// helpers +namespace +{ + +void cleanup(c_event::s_impl *ip_impl) +{ + if((nullptr != ip_impl) && (ip_impl->mp_events)) + { + delete[] ip_impl->mp_events; + ip_impl->mp_events = nullptr; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// walk through all events to find a pending signal, returns its index or +// c_event::timeout. Must be called under the mutex. +uint32_t get_signal(c_event::s_impl *ip_impl) +{ + uint32_t lu_return = c_event::timeout; + + if(0 == ip_impl->mi_signals) + { + return lu_return; + } + + c_event::s_impl::s_event *lp_start = ip_impl->mp_event_cur; + do + { + ip_impl->mp_event_cur = ip_impl->mp_event_cur->mp_next; + + if(ip_impl->mp_event_cur->mi_counter) + { + lu_return = ip_impl->mp_event_cur->mu_idx; + if(c_event::e_single_manual == ip_impl->mp_event_cur->me_type) + { + dispatch_semaphore_signal(ip_impl->mh_sem); // keep manual events level-triggered + } + else + { + ip_impl->mp_event_cur->mi_counter--; + ip_impl->mi_signals--; + } + + break; + } + } while(lp_start != ip_impl->mp_event_cur); + + if(c_event::timeout == lu_return) + { + ip_impl->mi_signals--; + } + + return lu_return; +} + +} // anonymous namespace + +//////////////////////////////////////////////////////////////////////////////// +// c_event::c_event +c_event::c_event() + : mp_impl(new s_impl) +{ + mp_impl->mh_sem = nullptr; + mp_impl->mu_count = 0; + mp_impl->mb_init = false; + mp_impl->mb_error = false; + mp_impl->mp_events = nullptr; + mp_impl->mp_event_cur = nullptr; + mp_impl->mi_signals = 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::~c_event +c_event::~c_event() +{ + if(mp_impl->mb_init) + { + pthread_mutex_destroy(&mp_impl->ms_mutex); + + if(mp_impl->mh_sem) + { + dispatch_release(mp_impl->mh_sem); + mp_impl->mh_sem = nullptr; + } + } + + cleanup(mp_impl); + + delete mp_impl; + mp_impl = nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::init +bool c_event::init(uint8_t iu_count, const e_type *ip_types) +{ + bool lb_sig = false; + bool lb_mutex = false; + uint8_t lu_idx = 0; + + if((true == mp_impl->mb_init) || (true == mp_impl->mb_error) || (0 >= iu_count) || (nullptr == ip_types)) + { + return false; + } + + //////////////////////////////////////////////////////////////////////////// + // initialize semaphore + mp_impl->mh_sem = dispatch_semaphore_create(0); + if(nullptr == mp_impl->mh_sem) + { + goto l_lblExit; + } + else + { + lb_sig = true; + } + + //////////////////////////////////////////////////////////////////////////// + // initialize mutex + if(0 != pthread_mutex_init(&mp_impl->ms_mutex, nullptr)) + { + mp_impl->mb_error = true; + goto l_lblExit; + } + else + { + lb_mutex = true; + } + + //////////////////////////////////////////////////////////////////////////// + // initialize events structure + mp_impl->mp_events = new s_impl::s_event[iu_count]; + if(nullptr == mp_impl->mp_events) + { + goto l_lblExit; + } + + memset(mp_impl->mp_events, 0, sizeof(s_impl::s_event) * iu_count); + + mp_impl->mp_event_cur = &mp_impl->mp_events[0]; + + while(lu_idx < iu_count) + { + mp_impl->mp_events[lu_idx].me_type = ip_types[lu_idx]; + mp_impl->mp_events[lu_idx].mi_counter = 0; + mp_impl->mp_events[lu_idx].mu_idx = lu_idx; + + // make round trip links + if((lu_idx + 1) < iu_count) + { + mp_impl->mp_events[lu_idx].mp_next = &mp_impl->mp_events[lu_idx + 1]; + } + else + { + mp_impl->mp_events[lu_idx].mp_next = &mp_impl->mp_events[0]; + } + + lu_idx++; + } + + mp_impl->mb_init = true; + mp_impl->mu_count = iu_count; + +l_lblExit: + if(false == mp_impl->mb_init) + { + mp_impl->mb_error = true; + + if(lb_mutex) + { + pthread_mutex_destroy(&mp_impl->ms_mutex); + } + + if(lb_sig) + { + dispatch_release(mp_impl->mh_sem); + mp_impl->mh_sem = nullptr; + } + + cleanup(mp_impl); + } + + return mp_impl->mb_init; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::set +bool c_event::set(uint32_t iu_id) +{ + if((iu_id >= mp_impl->mu_count) || (false == mp_impl->mb_init)) + { + return false; + } + + pthread_mutex_lock(&mp_impl->ms_mutex); + + mp_impl->mi_signals++; + mp_impl->mp_events[iu_id].mi_counter++; + + dispatch_semaphore_signal(mp_impl->mh_sem); + + pthread_mutex_unlock(&mp_impl->ms_mutex); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::clr +bool c_event::clr(uint32_t iu_id) +{ + bool lb_return = false; + + if((iu_id >= mp_impl->mu_count) || (e_single_manual != mp_impl->mp_events[iu_id].me_type)) + { + return lb_return; + } + + pthread_mutex_lock(&mp_impl->ms_mutex); + + if(mp_impl->mp_events[iu_id].mi_counter) + { + mp_impl->mi_signals--; + mp_impl->mp_events[iu_id].mi_counter--; + + dispatch_semaphore_wait(mp_impl->mh_sem, DISPATCH_TIME_NOW); // decrease semaphore + + lb_return = true; + } + + pthread_mutex_unlock(&mp_impl->ms_mutex); + + return lb_return; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::wait +uint32_t c_event::wait() +{ + uint32_t lu_return = c_event::timeout; + + dispatch_semaphore_wait(mp_impl->mh_sem, DISPATCH_TIME_FOREVER); + + pthread_mutex_lock(&mp_impl->ms_mutex); + lu_return = get_signal(mp_impl); + pthread_mutex_unlock(&mp_impl->ms_mutex); + + return lu_return; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::wait +uint32_t c_event::wait(uint32_t iu_timeout_ms) +{ + uint32_t lu_return = c_event::timeout; + dispatch_time_t lu_time = (0 == iu_timeout_ms) + ? DISPATCH_TIME_NOW + : dispatch_time(DISPATCH_TIME_NOW, (int64_t)iu_timeout_ms * (int64_t)NSEC_PER_MSEC); + + if(0 == dispatch_semaphore_wait(mp_impl->mh_sem, lu_time)) + { + pthread_mutex_lock(&mp_impl->ms_mutex); + lu_return = get_signal(mp_impl); + pthread_mutex_unlock(&mp_impl->ms_mutex); + } + + return lu_return; +} diff --git a/src/event/event_win32.cpp b/src/event/event_win32.cpp new file mode 100644 index 0000000..c653316 --- /dev/null +++ b/src/event/event_win32.cpp @@ -0,0 +1,189 @@ +#include "kit/event.hpp" +#include "kit/types.h" // pulls in (HANDLE, DWORD, CreateEvent, ...) via ts_helpers.h + +#include + +//////////////////////////////////////////////////////////////////////////////// +struct c_event::s_impl +{ + struct s_mevent + { + e_type me_type; + HANDLE mh_event; + }; + + uint8_t mu_count; + s_mevent *mp_events; + bool mb_init; + HANDLE *mp_handles; +}; + +//////////////////////////////////////////////////////////////////////////////// +// helpers +namespace +{ + +void cleanup(c_event::s_impl *ip_impl) +{ + if(nullptr == ip_impl) + { + return; + } + + if(ip_impl->mp_handles) + { + delete[] ip_impl->mp_handles; + ip_impl->mp_handles = nullptr; + } + + if(ip_impl->mp_events) + { + for(uint32_t lu_i = 0; lu_i < ip_impl->mu_count; lu_i++) + { + if(ip_impl->mp_events[lu_i].mh_event) + { + CloseHandle(ip_impl->mp_events[lu_i].mh_event); + ip_impl->mp_events[lu_i].mh_event = nullptr; + } + } + + delete[] ip_impl->mp_events; + ip_impl->mp_events = nullptr; + } +} + +} // anonymous namespace + +//////////////////////////////////////////////////////////////////////////////// +// c_event::c_event +c_event::c_event() + : mp_impl(new s_impl) +{ + mp_impl->mu_count = 0; + mp_impl->mp_events = nullptr; + mp_impl->mb_init = false; + mp_impl->mp_handles = nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::~c_event +c_event::~c_event() +{ + cleanup(mp_impl); + delete mp_impl; + mp_impl = nullptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::init +bool c_event::init(uint8_t iu_count, const e_type *ip_types) +{ + if((true == mp_impl->mb_init) || (0 >= iu_count) || (nullptr == ip_types)) + { + return false; + } + + uint8_t lu_idx = 0; + + mp_impl->mp_events = new s_impl::s_mevent[iu_count]; + if(nullptr == mp_impl->mp_events) + { + goto l_lblExit; + } + + mp_impl->mp_handles = new HANDLE[iu_count]; + if(nullptr == mp_impl->mp_handles) + { + goto l_lblExit; + } + + memset(mp_impl->mp_events, 0, sizeof(s_impl::s_mevent) * iu_count); + + while(lu_idx < iu_count) + { + mp_impl->mp_events[lu_idx].me_type = ip_types[lu_idx]; + + if(e_single_auto == mp_impl->mp_events[lu_idx].me_type) + { + mp_impl->mp_events[lu_idx].mh_event = CreateEvent(nullptr, FALSE, FALSE, nullptr); + } + else if(e_single_manual == mp_impl->mp_events[lu_idx].me_type) + { + mp_impl->mp_events[lu_idx].mh_event = CreateEvent(nullptr, TRUE, FALSE, nullptr); + } + else if(e_multi == mp_impl->mp_events[lu_idx].me_type) + { + mp_impl->mp_events[lu_idx].mh_event = CreateSemaphore(nullptr, 0, 0xFFFFFF, nullptr); + } + + mp_impl->mp_handles[lu_idx] = mp_impl->mp_events[lu_idx].mh_event; + + lu_idx++; + } + + mp_impl->mb_init = true; + mp_impl->mu_count = iu_count; + +l_lblExit: + if(false == mp_impl->mb_init) + { + cleanup(mp_impl); + } + + return mp_impl->mb_init; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::set +bool c_event::set(uint32_t iu_id) +{ + if(iu_id >= mp_impl->mu_count) + { + return false; + } + + if(e_multi == mp_impl->mp_events[iu_id].me_type) + { + ReleaseSemaphore(mp_impl->mp_events[iu_id].mh_event, 1, nullptr); + } + else + { + SetEvent(mp_impl->mp_events[iu_id].mh_event); + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::clr +bool c_event::clr(uint32_t iu_id) +{ + if((iu_id >= mp_impl->mu_count) || (e_single_manual != mp_impl->mp_events[iu_id].me_type)) + { + return false; + } + + ResetEvent(mp_impl->mp_events[iu_id].mh_event); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::wait +uint32_t c_event::wait() +{ + return wait(INFINITE); +} + +//////////////////////////////////////////////////////////////////////////////// +// c_event::wait +uint32_t c_event::wait(uint32_t iu_timeout_ms) +{ + DWORD lu_res = WaitForMultipleObjects(mp_impl->mu_count, mp_impl->mp_handles, FALSE, iu_timeout_ms); + + if((WAIT_TIMEOUT == lu_res) || (WAIT_FAILED == lu_res)) + { + return c_event::timeout; + } + + return (uint32_t)(lu_res - WAIT_OBJECT_0); +} diff --git a/tests/event_test.hpp b/tests/event_test.hpp new file mode 100644 index 0000000..4c73cee --- /dev/null +++ b/tests/event_test.hpp @@ -0,0 +1,227 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +// init with nullptr types / zero count fails +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, InitInvalidArgs) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_auto }; + + EXPECT_FALSE(lo_event.init(0, lp_types)); + EXPECT_FALSE(lo_event.init(1, nullptr)); +} + +//////////////////////////////////////////////////////////////////////////////// +// init twice fails (already initialized) +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, InitTwiceFails) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_auto }; + + ASSERT_TRUE(lo_event.init(1, lp_types)); + EXPECT_FALSE(lo_event.init(1, lp_types)); +} + +//////////////////////////////////////////////////////////////////////////////// +// single auto: set then wait returns its index, second wait times out +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, SingleAutoSetWait) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_auto }; + ASSERT_TRUE(lo_event.init(1, lp_types)); + + ASSERT_TRUE(lo_event.set(0)); + EXPECT_EQ(0u, lo_event.wait(1000)); + + // auto-reset: no more signals pending + EXPECT_EQ(c_event::timeout, lo_event.wait(0)); +} + +//////////////////////////////////////////////////////////////////////////////// +// wait with no signal times out +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, WaitTimeout) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_auto }; + ASSERT_TRUE(lo_event.init(1, lp_types)); + + auto lo_start = std::chrono::steady_clock::now(); + auto lu_result = lo_event.wait(50); + auto li_elapsed + = std::chrono::duration_cast(std::chrono::steady_clock::now() - lo_start).count(); + + EXPECT_EQ(c_event::timeout, lu_result); + EXPECT_GE(li_elapsed, 40); // allow some scheduling slack below the 50 ms request +} + +//////////////////////////////////////////////////////////////////////////////// +// single manual stays signaled across waits until clr() +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, SingleManualStaysSignaled) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_manual }; + ASSERT_TRUE(lo_event.init(1, lp_types)); + + ASSERT_TRUE(lo_event.set(0)); + + // manual-reset: repeated waits keep returning the same index + EXPECT_EQ(0u, lo_event.wait(1000)); + EXPECT_EQ(0u, lo_event.wait(1000)); + + ASSERT_TRUE(lo_event.clr(0)); + EXPECT_EQ(c_event::timeout, lo_event.wait(0)); +} + +//////////////////////////////////////////////////////////////////////////////// +// clr is rejected for non-manual events +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, ClrRejectedForAuto) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_auto }; + ASSERT_TRUE(lo_event.init(1, lp_types)); + + EXPECT_FALSE(lo_event.clr(0)); +} + +//////////////////////////////////////////////////////////////////////////////// +// multi (counting) accumulates multiple sets +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, MultiCounting) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_multi }; + ASSERT_TRUE(lo_event.init(1, lp_types)); + + ASSERT_TRUE(lo_event.set(0)); + ASSERT_TRUE(lo_event.set(0)); + ASSERT_TRUE(lo_event.set(0)); + + EXPECT_EQ(0u, lo_event.wait(1000)); + EXPECT_EQ(0u, lo_event.wait(1000)); + EXPECT_EQ(0u, lo_event.wait(1000)); + EXPECT_EQ(c_event::timeout, lo_event.wait(0)); +} + +//////////////////////////////////////////////////////////////////////////////// +// set / wait out of range +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, SetOutOfRange) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_auto, c_event::e_single_auto }; + ASSERT_TRUE(lo_event.init(2, lp_types)); + + EXPECT_FALSE(lo_event.set(2)); + EXPECT_FALSE(lo_event.clr(2)); +} + +//////////////////////////////////////////////////////////////////////////////// +// wait-any across several events returns a signaled index +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, WaitAny) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_auto, c_event::e_single_auto, c_event::e_single_auto }; + ASSERT_TRUE(lo_event.init(3, lp_types)); + + ASSERT_TRUE(lo_event.set(2)); + EXPECT_EQ(2u, lo_event.wait(1000)); + + ASSERT_TRUE(lo_event.set(0)); + EXPECT_EQ(0u, lo_event.wait(1000)); +} + +//////////////////////////////////////////////////////////////////////////////// +// producer thread signals while consumer blocks in wait() +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, ProducerConsumer) +{ + c_event lo_event; + const c_event::e_type lp_types[] = { c_event::e_single_auto }; + ASSERT_TRUE(lo_event.init(1, lp_types)); + + std::atomic lb_started { false }; + + std::thread lo_producer( + [&]() + { + lb_started.store(true, std::memory_order_release); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + lo_event.set(0); + }); + + while(!lb_started.load(std::memory_order_acquire)) + { + std::this_thread::yield(); + } + + EXPECT_EQ(0u, lo_event.wait(5000)); + + lo_producer.join(); +} + +//////////////////////////////////////////////////////////////////////////////// +// many producers each set their own auto event; consumer drains them all +//////////////////////////////////////////////////////////////////////////////// +TEST(c_event_test, ConcurrentProducers) +{ + static constexpr uint8_t EVENT_COUNT = 8; + + c_event lo_event; + std::vector lo_types(EVENT_COUNT, c_event::e_multi); + ASSERT_TRUE(lo_event.init(EVENT_COUNT, lo_types.data())); + + std::atomic lb_start { false }; + std::vector lo_threads; + lo_threads.reserve(EVENT_COUNT); + + for(uint8_t i = 0; i < EVENT_COUNT; i++) + { + lo_threads.emplace_back( + [&, i]() + { + while(!lb_start.load(std::memory_order_acquire)) + { + std::this_thread::yield(); + } + lo_event.set(i); + }); + } + + lb_start.store(true, std::memory_order_release); + + // drain exactly EVENT_COUNT signals and verify each index appears once + std::vector lo_seen(EVENT_COUNT, 0); + for(uint8_t i = 0; i < EVENT_COUNT; i++) + { + uint32_t lu_idx = lo_event.wait(5000); + ASSERT_NE(c_event::timeout, lu_idx); + ASSERT_LT(lu_idx, (uint32_t)EVENT_COUNT); + lo_seen[lu_idx]++; + } + + for(auto &t : lo_threads) + { + t.join(); + } + + for(uint8_t i = 0; i < EVENT_COUNT; i++) + { + EXPECT_EQ(1, lo_seen[i]) << "event " << (int)i << " signaled " << lo_seen[i] << " times"; + } + + EXPECT_EQ(c_event::timeout, lo_event.wait(0)); +} diff --git a/tests/main.cpp b/tests/main.cpp index 1535523..3bd1b4f 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -5,6 +5,7 @@ #include "list_test.hpp" #include "spin_lock_test.hpp" #include "shared_test.hpp" +#include "event_test.hpp" TEST(VersionTest, ReturnsNonEmpty) {