From cebab604d74a3c254dec316be43f28dbeaf2049e Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 19:18:24 +0900 Subject: [PATCH 1/6] feat(#494): Add C11 threads.h backend with auto-detection and fallback Add thread_c11.c implementing C11 thrd_create/mtx_*/cnd_* API with a trampoline to bridge void*(*)(void*) to int(*)(void*). Update thread.h with 3-tier ifdef cascade (C11 > Win32 > POSIX). Meson detects C11 support via cc.links() with actual thrd_create call. io_adapter.c uses call_once + mtx_init for lazy mutex init on C11 path with error flag propagation. POSIX and MSVC backends unchanged. --- wirelog/io/io_adapter.c | 37 ++++++--- wirelog/meson.build | 24 +++++- wirelog/thread.h | 59 +++++++++----- wirelog/thread_c11.c | 176 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 263 insertions(+), 33 deletions(-) create mode 100644 wirelog/thread_c11.c diff --git a/wirelog/io/io_adapter.c b/wirelog/io/io_adapter.c index 8e3c269e..7abb3d22 100644 --- a/wirelog/io/io_adapter.c +++ b/wirelog/io/io_adapter.c @@ -69,15 +69,24 @@ typedef struct { static registry_entry_t s_registry[WL_IO_MAX_ADAPTERS]; static uint32_t s_count; -/* POSIX: statically initialized via PTHREAD_MUTEX_INITIALIZER. - * Windows: CRITICAL_SECTION cannot be statically initialized, so we - * use a two-phase flag + InterlockedCompareExchange for one-shot init. +/* Mutex initialization strategy (3-tier, Issue #494): * - * Two-phase flag is required to avoid a race where one thread sets the - * flag to "done" via ICS and then starts mutex_init(), while a second - * thread sees "done" and calls mutex_lock() before mutex_init() returns. - * See Issue #459 for background. */ -#if defined(_WIN32) || defined(_WIN64) + * C11 threads: No static initializer for mtx_t. Use call_once() to + * lazily call mtx_init() on first access. + * POSIX: Statically initialized via PTHREAD_MUTEX_INITIALIZER. + * Windows: CRITICAL_SECTION cannot be statically initialized, so we + * use a two-phase flag + InterlockedCompareExchange. + * See Issue #459 for background. */ +#if defined(WL_HAVE_C11_THREADS) +static mutex_t s_mutex; +static once_flag s_mutex_once = ONCE_FLAG_INIT; +static int s_mutex_init_ok; +static void +init_mutex(void) +{ + s_mutex_init_ok = (mutex_init(&s_mutex) == 0); +} +#elif defined(_WIN32) || defined(_WIN64) static mutex_t s_mutex; /* 0 = uninitialised, 1 = initialisation in progress, 2 = ready */ static volatile long s_mutex_init; @@ -96,13 +105,19 @@ extern const wl_io_adapter_t wl_csv_adapter; static void ensure_builtins(void) { - /* On Windows, CRITICAL_SECTION requires explicit init. - * Two-phase flag protocol (Issue #459): + /* Lazy mutex initialization for backends without static initializers. */ +#if defined(WL_HAVE_C11_THREADS) + call_once(&s_mutex_once, init_mutex); + if (!s_mutex_init_ok) { + set_error("mutex initialization failed"); + return; + } +#elif defined(_WIN32) || defined(_WIN64) + /* Two-phase flag protocol (Issue #459): * 0 -> 1: winning thread claims init (ICS); all others spin. * 1 -> 2: winning thread sets "done" only after mutex_init returns. * This prevents a competing thread from calling mutex_lock on a * CRITICAL_SECTION that has not yet been initialised. */ -#if defined(_WIN32) || defined(_WIN64) if (InterlockedCompareExchange(&s_mutex_init, 2, 2) != 2) { if (InterlockedCompareExchange(&s_mutex_init, 1, 0) == 0) { mutex_init(&s_mutex); diff --git a/wirelog/meson.build b/wirelog/meson.build index 3ec1623f..db6190d1 100644 --- a/wirelog/meson.build +++ b/wirelog/meson.build @@ -37,11 +37,31 @@ wirelog_arena_src = files('arena/arena.c', ) #Thread Backend Module(Issue #88 - Cross-Platform Threading) #== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == -# Select threading backend based on platform -if host_machine.system() == 'windows' +# Select threading backend: C11 threads.h > POSIX pthreads > Windows MSVC +# C11 threads.h detection uses cc.links() to verify actual support, +# not just header presence (Apple Clang ships no threads.h). +c11_threads_test = ''' +#include +static int dummy(void *arg) { (void)arg; return 0; } +int main(void) { + thrd_t t; + if (thrd_create(&t, dummy, 0) == thrd_success) + thrd_join(t, 0); + return 0; +} +''' +have_c11_threads = cc.links(c11_threads_test, name: 'C11 support') + +if have_c11_threads + wirelog_thread_src = files('thread_c11.c', ) + add_project_arguments('-DWL_HAVE_C11_THREADS', language: 'c') + message(' Threading backend: C11 ') +elif host_machine.system() == 'windows' wirelog_thread_src = files('thread_msvc.c', ) + message(' Threading backend: Windows MSVC') else wirelog_thread_src = files('thread_posix.c', ) + message(' Threading backend: POSIX pthreads') endif #== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == diff --git a/wirelog/thread.h b/wirelog/thread.h index c989f709..7697f64d 100644 --- a/wirelog/thread.h +++ b/wirelog/thread.h @@ -11,13 +11,14 @@ * Overview * ======================================================================== * - * Platform-agnostic threading interface supporting both POSIX (pthread) - * and Windows (MSVC) threading models. + * Platform-agnostic threading interface with three backends, selected + * at build time by meson: * - * This abstraction layer allows the work queue and other threading code - * to remain platform-independent while supporting: - * - Unix/Linux/macOS via POSIX pthreads - * - Windows via MSVC threading APIs (CreateThread, CRITICAL_SECTION, etc.) + * 1. C11 (preferred, Linux GCC 10+/glibc 2.28+, musl) + * 2. POSIX pthreads (fallback, macOS Apple Clang, older Linux) + * 3. Windows MSVC (CreateThread, CRITICAL_SECTION, etc.) + * + * Detection order: WL_HAVE_C11_THREADS > _WIN32 > POSIX (default). * * ======================================================================== * Thread Safety Guarantees @@ -58,7 +59,9 @@ /* Platform-Specific Includes */ /* ======================================================================== */ -#if defined(_WIN32) || defined(_WIN64) +#if defined(WL_HAVE_C11_THREADS) +#include +#elif defined(_WIN32) || defined(_WIN64) #include #else #include @@ -71,10 +74,16 @@ /** * thread_t: * - * Thread handle. Platform-specific (pthread_t on POSIX, HANDLE on Windows). - * Embed by value or allocate as needed. + * Thread handle. Backend-specific: + * C11: thrd_t + * POSIX: pthread_t + * Win32: HANDLE */ -#if defined(_WIN32) || defined(_WIN64) +#if defined(WL_HAVE_C11_THREADS) +typedef struct thread_t { + thrd_t tid; +} thread_t; +#elif defined(_WIN32) || defined(_WIN64) typedef struct thread_t { HANDLE handle; } thread_t; @@ -87,18 +96,22 @@ typedef struct thread_t { /** * mutex_t: * - * Mutex (mutual exclusion lock). Platform-specific - * (pthread_mutex_t on POSIX, CRITICAL_SECTION on Windows). + * Mutex (mutual exclusion lock). Backend-specific: + * C11: mtx_t + * POSIX: pthread_mutex_t + * Win32: CRITICAL_SECTION * - * Non-recursive on POSIX: attempting to lock a pthread_mutex_t held by - * the same thread will deadlock (standard mutex semantics). + * Non-recursive semantics on all backends. * * NOTE: Windows CRITICAL_SECTION is reentrant/recursive by default, but * the public interface guarantees non-recursive semantics. Code must not - * rely on self-reentrance. If future code requires self-deadlock detection - * for safety, the MSVC backend can be enhanced with thread-ID tracking. + * rely on self-reentrance. */ -#if defined(_WIN32) || defined(_WIN64) +#if defined(WL_HAVE_C11_THREADS) +typedef struct mutex_t { + mtx_t m; +} mutex_t; +#elif defined(_WIN32) || defined(_WIN64) typedef struct mutex_t { CRITICAL_SECTION cs; } mutex_t; @@ -111,10 +124,16 @@ typedef struct mutex_t { /** * cond_t: * - * Condition variable. Platform-specific - * (pthread_cond_t on POSIX, CONDITION_VARIABLE on Windows). + * Condition variable. Backend-specific: + * C11: cnd_t + * POSIX: pthread_cond_t + * Win32: CONDITION_VARIABLE */ -#if defined(_WIN32) || defined(_WIN64) +#if defined(WL_HAVE_C11_THREADS) +typedef struct cond_t { + cnd_t c; +} cond_t; +#elif defined(_WIN32) || defined(_WIN64) typedef struct cond_t { CONDITION_VARIABLE cv; } cond_t; diff --git a/wirelog/thread_c11.c b/wirelog/thread_c11.c new file mode 100644 index 00000000..e0db18f2 --- /dev/null +++ b/wirelog/thread_c11.c @@ -0,0 +1,176 @@ +/* + * thread_c11.c - wirelog C11 Threading Backend (threads.h) + * + * Copyright (C) CleverPlant + * Licensed under LGPL-3.0 + * For commercial licenses, contact: inquiry@cleverplant.com + * + * INTERNAL - not installed, not part of public API. + * + * C11 standard threads implementation of the threading abstraction layer + * defined in thread.h. This backend is preferred when the compiler and + * platform support (e.g., GCC 10+/glibc 2.28+, musl). + * On platforms without C11 threads support (Apple Clang, older MSVC), + * the POSIX or MSVC backend is used instead. + * + * Issue #494: Migrate from POSIX pthreads to C11 + */ + +#include "thread.h" + +#include +#include + +/* ======================================================================== */ +/* Trampoline for C11 thrd_create */ +/* ======================================================================== */ + +/* + * C11 thrd_create expects int(*)(void*), but wirelog's thread_create + * uses void*(*)(void*) (matching the pthread signature). We use a + * thin trampoline to bridge the two. + */ +typedef struct { + void *(*fn)(void *); + void *arg; +} trampoline_ctx_t; + +static int +trampoline(void *raw) +{ + trampoline_ctx_t ctx = *(trampoline_ctx_t *)raw; + free(raw); + ctx.fn(ctx.arg); + return 0; +} + +/* ======================================================================== */ +/* Thread Creation and Joining */ +/* ======================================================================== */ + +int +thread_create(thread_t *tid, void *(*fn)(void *arg), void *arg) +{ + if (!tid || !fn) + return -1; + + trampoline_ctx_t *ctx = malloc(sizeof(*ctx)); + if (!ctx) + return -1; + + ctx->fn = fn; + ctx->arg = arg; + + int result = thrd_create(&tid->tid, trampoline, ctx); + if (result != thrd_success) { + free(ctx); + return -1; + } + return 0; +} + +int +thread_join(thread_t *tid) +{ + if (!tid) + return -1; + + int result = thrd_join(tid->tid, NULL); + return (result == thrd_success) ? 0 : -1; +} + +/* ======================================================================== */ +/* Mutex (Mutual Exclusion Lock) */ +/* ======================================================================== */ + +int +mutex_init(mutex_t *m) +{ + if (!m) + return -1; + + int result = mtx_init(&m->m, mtx_plain); + return (result == thrd_success) ? 0 : -1; +} + +int +mutex_lock(mutex_t *m) +{ + if (!m) + return -1; + + int result = mtx_lock(&m->m); + return (result == thrd_success) ? 0 : -1; +} + +int +mutex_unlock(mutex_t *m) +{ + if (!m) + return -1; + + int result = mtx_unlock(&m->m); + return (result == thrd_success) ? 0 : -1; +} + +void +mutex_destroy(mutex_t *m) +{ + if (!m) + return; + + mtx_destroy(&m->m); +} + +/* ======================================================================== */ +/* Condition Variable */ +/* ======================================================================== */ + +int +cond_init(cond_t *c) +{ + if (!c) + return -1; + + int result = cnd_init(&c->c); + return (result == thrd_success) ? 0 : -1; +} + +int +cond_wait(cond_t *c, mutex_t *m) +{ + if (!c || !m) + return -1; + + int result = cnd_wait(&c->c, &m->m); + return (result == thrd_success) ? 0 : -1; +} + +int +cond_signal(cond_t *c) +{ + if (!c) + return -1; + + int result = cnd_signal(&c->c); + return (result == thrd_success) ? 0 : -1; +} + +int +cond_broadcast(cond_t *c) +{ + if (!c) + return -1; + + int result = cnd_broadcast(&c->c); + return (result == thrd_success) ? 0 : -1; +} + +void +cond_destroy(cond_t *c) +{ + if (!c) + return; + + cnd_destroy(&c->c); +} From 7799c0665145cab7f0bacfb995ff65dfb8067a5d Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 19:18:35 +0900 Subject: [PATCH 2/6] refactor(#494): Migrate test threading from raw pthread to thread.h Replace direct pthread_create/pthread_join/pthread_t usage in test_io_adapter_concurrent.c and test_mem_ledger.c with the thread.h abstraction layer (thread_create/thread_join/thread_t). Enables these tests to run on the C11 threads backend without modification. --- tests/test_io_adapter_concurrent.c | 28 +++++----- tests/test_mem_ledger.c | 82 +++++++++++++++--------------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/tests/test_io_adapter_concurrent.c b/tests/test_io_adapter_concurrent.c index 5da120be..e0123f9f 100644 --- a/tests/test_io_adapter_concurrent.c +++ b/tests/test_io_adapter_concurrent.c @@ -31,12 +31,12 @@ static int passed = 0, failed = 0; /* ======================================================================== */ -/* Concurrent Tests (POSIX only; skipped on Windows) */ +/* Concurrent Tests (POSIX/C11; skipped on Windows) */ /* ======================================================================== */ #ifndef _WIN32 -#include +#include "wirelog/thread.h" #define NTHREADS 8 #define FIND_ITERS 2000 @@ -60,11 +60,11 @@ test_concurrent_find(void) { TEST("concurrent find (csv built-in, 8 threads x 2000 iters)"); - pthread_t threads[NTHREADS]; + thread_t threads[NTHREADS]; for (int i = 0; i < NTHREADS; i++) - pthread_create(&threads[i], NULL, find_worker, NULL); + thread_create(&threads[i], find_worker, NULL); for (int i = 0; i < NTHREADS; i++) - pthread_join(threads[i], NULL); + thread_join(&threads[i]); /* After concurrent reads, csv must still be findable */ const wl_io_adapter_t *a = wl_io_find_adapter("csv"); @@ -109,7 +109,7 @@ test_concurrent_register_unregister(void) { TEST("concurrent register/unregister (8 threads x 2000 iters each)"); - pthread_t threads[NTHREADS]; + thread_t threads[NTHREADS]; reg_arg_t args[NTHREADS]; for (int i = 0; i < NTHREADS; i++) { @@ -118,10 +118,10 @@ test_concurrent_register_unregister(void) args[i].adapter.scheme = args[i].scheme; args[i].adapter.abi_version = WL_IO_ABI_VERSION; args[i].result = 0; - pthread_create(&threads[i], NULL, reg_unreg_worker, &args[i]); + thread_create(&threads[i], reg_unreg_worker, &args[i]); } for (int i = 0; i < NTHREADS; i++) - pthread_join(threads[i], NULL); + thread_join(&threads[i]); int any_failed = 0; for (int i = 0; i < NTHREADS; i++) { @@ -165,8 +165,8 @@ test_mixed_readers_writers(void) { TEST("mixed readers + writers (4 find + 4 reg/unreg threads)"); - pthread_t readers[4]; - pthread_t writers[4]; + thread_t readers[4]; + thread_t writers[4]; reg_arg_t wargs[4]; for (int i = 0; i < 4; i++) { @@ -174,12 +174,12 @@ test_mixed_readers_writers(void) memset(&wargs[i].adapter, 0, sizeof(wargs[i].adapter)); wargs[i].adapter.scheme = wargs[i].scheme; wargs[i].adapter.abi_version = WL_IO_ABI_VERSION; - pthread_create(&readers[i], NULL, mixed_find_worker, NULL); - pthread_create(&writers[i], NULL, mixed_reg_worker, &wargs[i]); + thread_create(&readers[i], mixed_find_worker, NULL); + thread_create(&writers[i], mixed_reg_worker, &wargs[i]); } for (int i = 0; i < 4; i++) { - pthread_join(readers[i], NULL); - pthread_join(writers[i], NULL); + thread_join(&readers[i]); + thread_join(&writers[i]); } /* Registry must still be consistent: csv findable, no dangling entries */ diff --git a/tests/test_mem_ledger.c b/tests/test_mem_ledger.c index 1b910e88..00a200e7 100644 --- a/tests/test_mem_ledger.c +++ b/tests/test_mem_ledger.c @@ -9,7 +9,7 @@ * 1. alloc/free tracking accuracy * 2. budget enforcement (over_budget) * 3. peak_bytes high-water mark - * 4. concurrent multi-pthread consistency + * 4. concurrent thread consistency * 5. human-readable report output (smoke test) * 6. subsystem over-budget detection * 7. backpressure threshold @@ -25,7 +25,7 @@ #include #ifndef _WIN32 -#include +#include "wirelog/thread.h" #endif /* ======================================================================== */ @@ -37,20 +37,20 @@ static int tests_passed = 0; static int tests_failed = 0; #define TEST(name) \ - do { \ - tests_run++; \ - printf(" [%d] %s", tests_run, name); \ - } while (0) + do { \ + tests_run++; \ + printf(" [%d] %s", tests_run, name); \ + } while (0) #define PASS() \ - do { \ - tests_passed++; \ - printf(" ... PASS\n"); \ - } while (0) + do { \ + tests_passed++; \ + printf(" ... PASS\n"); \ + } while (0) #define FAIL(msg) \ - do { \ - tests_failed++; \ - printf(" ... FAIL: %s\n", (msg)); \ - } while (0) + do { \ + tests_failed++; \ + printf(" ... FAIL: %s\n", (msg)); \ + } while (0) /* ======================================================================== */ /* Test 1: alloc/free tracking accuracy */ @@ -69,7 +69,7 @@ test_alloc_free_accuracy(void) wl_mem_ledger_alloc(&ledger, WL_MEM_SUBSYS_CACHE, 256); uint64_t cur = (uint64_t)atomic_load_explicit(&ledger.current_bytes, - memory_order_relaxed); + memory_order_relaxed); uint64_t rel = (uint64_t)atomic_load_explicit( &ledger.subsys_bytes[WL_MEM_SUBSYS_RELATION], memory_order_relaxed); uint64_t arena = (uint64_t)atomic_load_explicit( @@ -78,7 +78,7 @@ test_alloc_free_accuracy(void) if (cur != 1792) { char msg[64]; snprintf(msg, sizeof(msg), "current_bytes=%llu, want 1792", - (unsigned long long)cur); + (unsigned long long)cur); FAIL(msg); return 1; } @@ -93,14 +93,14 @@ test_alloc_free_accuracy(void) wl_mem_ledger_free(&ledger, WL_MEM_SUBSYS_RELATION, 512); cur = (uint64_t)atomic_load_explicit(&ledger.current_bytes, - memory_order_relaxed); + memory_order_relaxed); rel = (uint64_t)atomic_load_explicit( &ledger.subsys_bytes[WL_MEM_SUBSYS_RELATION], memory_order_relaxed); if (cur != 1280) { char msg[64]; snprintf(msg, sizeof(msg), "current_bytes=%llu after free, want 1280", - (unsigned long long)cur); + (unsigned long long)cur); FAIL(msg); return 1; } @@ -153,7 +153,7 @@ test_budget_enforcement(void) wl_mem_ledger_t unlimited; wl_mem_ledger_init(&unlimited, 0); wl_mem_ledger_alloc(&unlimited, WL_MEM_SUBSYS_RELATION, - UINT64_MAX / 2); /* enormous */ + UINT64_MAX / 2); /* enormous */ if (wl_mem_ledger_over_budget(&unlimited)) { FAIL("unlimited budget (0) should never be over_budget"); return 1; @@ -177,11 +177,11 @@ test_peak_high_water(void) wl_mem_ledger_alloc(&ledger, WL_MEM_SUBSYS_RELATION, 2000); uint64_t peak1 = (uint64_t)atomic_load_explicit(&ledger.peak_bytes, - memory_order_relaxed); + memory_order_relaxed); if (peak1 != 2000) { char msg[64]; snprintf(msg, sizeof(msg), "peak=%llu after 2000 alloc, want 2000", - (unsigned long long)peak1); + (unsigned long long)peak1); FAIL(msg); return 1; } @@ -189,7 +189,7 @@ test_peak_high_water(void) /* Free does not lower peak */ wl_mem_ledger_free(&ledger, WL_MEM_SUBSYS_RELATION, 2000); uint64_t peak2 = (uint64_t)atomic_load_explicit(&ledger.peak_bytes, - memory_order_relaxed); + memory_order_relaxed); if (peak2 != 2000) { FAIL("peak_bytes decreased after free (should be HWM)"); return 1; @@ -198,7 +198,7 @@ test_peak_high_water(void) /* New alloc below old HWM does not change peak */ wl_mem_ledger_alloc(&ledger, WL_MEM_SUBSYS_ARENA, 500); uint64_t peak3 = (uint64_t)atomic_load_explicit(&ledger.peak_bytes, - memory_order_relaxed); + memory_order_relaxed); if (peak3 != 2000) { FAIL("peak_bytes changed for alloc below HWM"); return 1; @@ -207,11 +207,11 @@ test_peak_high_water(void) /* Alloc above old HWM updates peak */ wl_mem_ledger_alloc(&ledger, WL_MEM_SUBSYS_ARENA, 2000); uint64_t peak4 = (uint64_t)atomic_load_explicit(&ledger.peak_bytes, - memory_order_relaxed); + memory_order_relaxed); if (peak4 != 2500) { char msg[64]; snprintf(msg, sizeof(msg), "peak=%llu, want 2500", - (unsigned long long)peak4); + (unsigned long long)peak4); FAIL(msg); return 1; } @@ -222,7 +222,7 @@ test_peak_high_water(void) if (arena_peak != 2500) { char msg[64]; snprintf(msg, sizeof(msg), "ARENA subsys_peak=%llu, want 2500", - (unsigned long long)arena_peak); + (unsigned long long)arena_peak); FAIL(msg); return 1; } @@ -232,7 +232,7 @@ test_peak_high_water(void) } /* ======================================================================== */ -/* Test 4: concurrent multi-pthread consistency (Unix-like systems only) */ +/* Test 4: concurrent thread consistency (Unix-like systems only) */ /* ======================================================================== */ #ifndef _WIN32 @@ -260,38 +260,38 @@ conc_worker(void *arg) static int test_concurrent_consistency(void) { - TEST("concurrent multi-pthread consistency"); + TEST("concurrent thread consistency"); wl_mem_ledger_t ledger; wl_mem_ledger_init(&ledger, 0); - pthread_t threads[CONC_THREADS]; + thread_t threads[CONC_THREADS]; conc_arg_t args[CONC_THREADS]; for (int i = 0; i < CONC_THREADS; i++) { args[i].ledger = &ledger; args[i].subsys = i % WL_MEM_SUBSYS_COUNT; - pthread_create(&threads[i], NULL, conc_worker, &args[i]); + thread_create(&threads[i], conc_worker, &args[i]); } for (int i = 0; i < CONC_THREADS; i++) { - pthread_join(threads[i], NULL); + thread_join(&threads[i]); } /* After equal alloc/free cycles, current_bytes must be 0 */ uint64_t cur = (uint64_t)atomic_load_explicit(&ledger.current_bytes, - memory_order_relaxed); + memory_order_relaxed); if (cur != 0) { char msg[80]; snprintf(msg, sizeof(msg), - "current_bytes=%llu after balanced alloc/free, want 0", - (unsigned long long)cur); + "current_bytes=%llu after balanced alloc/free, want 0", + (unsigned long long)cur); FAIL(msg); return 1; } /* Peak must be > 0 (some concurrent allocation occurred) */ uint64_t peak = (uint64_t)atomic_load_explicit(&ledger.peak_bytes, - memory_order_relaxed); + memory_order_relaxed); if (peak == 0) { FAIL("peak_bytes==0 after concurrent allocs (unexpected)"); return 1; @@ -316,11 +316,11 @@ test_report_output(void) wl_mem_ledger_init(&ledger, (uint64_t)48 * 1024 * 1024 * 1024); /* 48GB */ wl_mem_ledger_alloc(&ledger, WL_MEM_SUBSYS_RELATION, - (uint64_t)12 * 1024 * 1024 * 1024); /* 12GB */ + (uint64_t)12 * 1024 * 1024 * 1024); /* 12GB */ wl_mem_ledger_alloc(&ledger, WL_MEM_SUBSYS_ARENA, - (uint64_t)2 * 1024 * 1024 * 1024); + (uint64_t)2 * 1024 * 1024 * 1024); wl_mem_ledger_alloc(&ledger, WL_MEM_SUBSYS_CACHE, - (uint64_t)500 * 1024 * 1024); + (uint64_t)500 * 1024 * 1024); /* Just verify it doesn't crash; output goes to stderr */ wl_mem_ledger_report(&ledger); @@ -420,7 +420,7 @@ test_bytes_remaining(void) if (rem != 1000) { char msg[64]; snprintf(msg, sizeof(msg), "remaining=%llu, want 1000", - (unsigned long long)rem); + (unsigned long long)rem); FAIL(msg); return 1; } @@ -430,7 +430,7 @@ test_bytes_remaining(void) if (rem != 700) { char msg[64]; snprintf(msg, sizeof(msg), "remaining=%llu after 300 alloc, want 700", - (unsigned long long)rem); + (unsigned long long)rem); FAIL(msg); return 1; } @@ -441,7 +441,7 @@ test_bytes_remaining(void) if (rem != 0) { char msg[64]; snprintf(msg, sizeof(msg), "remaining=%llu when over budget, want 0", - (unsigned long long)rem); + (unsigned long long)rem); FAIL(msg); return 1; } From f70f6cd00bb206b1aaef32105a24e68191e1be6f Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 19:18:44 +0900 Subject: [PATCH 3/6] docs(#494): Add C11 threading backend CHANGELOG entry Document C11 backend addition with auto-detection and POSIX/MSVC fallback under [Unreleased] section. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 211d6b62..9e8e7c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to wirelog are documented in this file. - **wl_easy Facade** (#445): Simplified high-level API (`wl_easy.h`) for common session workflows - **String Operations** (#444): String-typed column functions (`strlen`, `cat`, `substr`, `contains`, `to_upper`, `to_lower`, `trim`, `str_replace`, `to_string`, `to_number`) - **Path A Example** (#462): Standalone pcap adapter skeleton with CI compile-check against installed headers +- **C11 Threading Backend** (#494): Add C11 `` backend with auto-detection; POSIX/MSVC fallback preserved. `call_once` pattern for adapter registry initialization - **Binary Size Gate** (#460): CI regression gate for `.text` section growth (5KB budget) - **I/O Adapters User Guide** (#463): `docs/io-adapters.md` with Path A/B workflows, ABI policy, ownership rules, and thread-safety notes - **Retraction Support** (#443): Fact retraction with recursive re-evaluation From 4cd7dc440476da6a03a81fe57a8782563a081459 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 20:17:26 +0900 Subject: [PATCH 4/6] fix(#494): Unify thread backend selection in tests and benchmarks Replace hardcoded thread_posix.c/thread_msvc.c selection in tests/meson.build and bench/meson.build with wirelog_thread_src from the parent scope. Fixes type mismatch when C11 threads backend is active (mtx_t vs pthread_mutex_t under TSan). --- bench/meson.build | 9 +++------ tests/meson.build | 10 +++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/bench/meson.build b/bench/meson.build index 59a4228c..b607ac65 100644 --- a/bench/meson.build +++ b/bench/meson.build @@ -64,12 +64,9 @@ bench_workqueue_src = files( subdir_wirelog / 'workqueue.c', ) -# Select threading backend based on platform -if host_machine.system() == 'windows' - bench_thread_src = files(subdir_wirelog / 'thread_msvc.c') -else - bench_thread_src = files(subdir_wirelog / 'thread_posix.c') -endif +# Reuse the same threading backend selected by wirelog/meson.build +# (C11 threads.h > POSIX pthreads > Windows MSVC). +bench_thread_src = wirelog_thread_src # Skip bench_flowlog on Windows (uses POSIX getopt) if host_machine.system() != 'windows' diff --git a/tests/meson.build b/tests/meson.build index 4bc62c50..d3defdb8 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -50,11 +50,11 @@ io_src = files( ) # Select threading backend based on platform -if host_machine.system() == 'windows' - thread_src = files('../wirelog/thread_msvc.c',) -else - thread_src = files('../wirelog/thread_posix.c',) -endif +# Reuse the same threading backend selected by wirelog/meson.build +# (C11 threads.h > POSIX pthreads > Windows MSVC). +# wirelog_thread_src is defined in wirelog/meson.build and propagated +# to subdir builds via the parent scope. +thread_src = wirelog_thread_src # ============================================================================ # Test Executables From b15224f2b6c2571530cbb2bff0e83d75022920e0 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Tue, 14 Apr 2026 21:06:51 +0900 Subject: [PATCH 5/6] fix(#494): Use atomic_int for s_mutex_init_ok to satisfy TSan TSan does not track happens-before through glibc call_once for non-atomic variables. Use atomic_store/atomic_load on the init flag to make the synchronization explicit and TSan-clean. --- wirelog/io/io_adapter.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wirelog/io/io_adapter.c b/wirelog/io/io_adapter.c index 7abb3d22..87914d3e 100644 --- a/wirelog/io/io_adapter.c +++ b/wirelog/io/io_adapter.c @@ -78,13 +78,14 @@ static uint32_t s_count; * use a two-phase flag + InterlockedCompareExchange. * See Issue #459 for background. */ #if defined(WL_HAVE_C11_THREADS) +#include static mutex_t s_mutex; static once_flag s_mutex_once = ONCE_FLAG_INIT; -static int s_mutex_init_ok; +static atomic_int s_mutex_init_ok; static void init_mutex(void) { - s_mutex_init_ok = (mutex_init(&s_mutex) == 0); + atomic_store(&s_mutex_init_ok, (mutex_init(&s_mutex) == 0) ? 1 : 0); } #elif defined(_WIN32) || defined(_WIN64) static mutex_t s_mutex; @@ -108,7 +109,7 @@ ensure_builtins(void) /* Lazy mutex initialization for backends without static initializers. */ #if defined(WL_HAVE_C11_THREADS) call_once(&s_mutex_once, init_mutex); - if (!s_mutex_init_ok) { + if (!atomic_load(&s_mutex_init_ok)) { set_error("mutex initialization failed"); return; } From 4afdfb7b44b97790d49ed785cb146ab21f4102b5 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Wed, 15 Apr 2026 10:48:51 +0900 Subject: [PATCH 6/6] fix(#494): Add -Dthreads=posix option for TSan compatibility TSan only intercepts POSIX pthread calls; C11 functions (thrd_create, mtx_lock) are uninstrumented, causing false SEGV. Split threads option into native (auto-detect C11/POSIX) and posix (force pthreads). CI TSan steps now use -Dthreads=posix to ensure proper instrumentation. Remove disabled option since threading is always required. --- .github/workflows/ci-main.yml | 2 ++ .github/workflows/ci-pr.yml | 4 ++++ meson.build | 18 ++++++++---------- meson_options.txt | 6 +++--- wirelog/meson.build | 11 ++++++++++- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index e3ee9057..42e31588 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -267,11 +267,13 @@ jobs: sudo apt-get install -y clang fi + # Force POSIX pthreads for TSan (C11 threads.h is uninstrumented) - name: Configure with TSan run: > meson setup builddir-tsan -Db_sanitize=thread -Db_lundef=false + -Dthreads=posix -Dtests=true --buildtype=debug env: diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 9e52819e..b5ff93bc 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -170,12 +170,16 @@ jobs: ASAN_OPTIONS: abort_on_error=1:halt_on_error=1 UBSAN_OPTIONS: abort_on_error=1:halt_on_error=1:print_stacktrace=1 + # TSan: force POSIX pthreads via -Dthreads=posix. + # TSan only intercepts pthread calls; C11 functions + # (thrd_create, mtx_lock) are uninstrumented and cause false SEGV. - name: Sanitizers / TSan if: matrix.os == 'ubuntu-latest' run: | meson setup builddir-tsan \ -Db_sanitize=thread \ -Db_lundef=false \ + -Dthreads=posix \ -Dtests=true \ --buildtype=debug meson compile -C builddir-tsan diff --git a/meson.build b/meson.build index e0ed632b..8e5f7dd1 100644 --- a/meson.build +++ b/meson.build @@ -97,16 +97,14 @@ message(' Plugin loader (dlopen): ' + io_plugin_dlopen_option) #Dependencies #== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == -#Optional : pthreads for threading +#pthreads for threading threads_dep = [] -if threads_option == 'enabled' - _td = dependency('threads', required: false) - if not _td.found() - _td = cc.find_library('pthread', required: false) - endif - if _td.found() - threads_dep = [_td] - endif +_td = dependency('threads', required: false) +if not _td.found() + _td = cc.find_library('pthread', required: false) +endif +if _td.found() + threads_dep = [_td] endif #Optional : zlib for IPC compression(future) @@ -180,7 +178,7 @@ config_h.set_quoted('WIRELOG_VERSION', project_version) config_h.set_quoted('WIRELOG_PLATFORM', platform) config_h.set('WIRELOG_EMBEDDED', is_embedded) config_h.set('WIRELOG_IPC', ipc_option == 'enabled') -config_h.set('WIRELOG_THREADS', threads_option == 'enabled') +config_h.set('WIRELOG_THREADS', true) #Generate config header config_h_file = configure_file( diff --git a/meson_options.txt b/meson_options.txt index 801ed30f..26fe396b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -16,9 +16,9 @@ option( option( 'threads', type: 'combo', - choices: ['disabled', 'enabled'], - value: 'enabled', - description: 'Enable pthreads support for multi-threaded execution' + choices: ['native', 'posix'], + value: 'native', + description: 'Threading backend: native (auto-detect C11/POSIX) or posix (force pthreads, required for TSan)' ) option( diff --git a/wirelog/meson.build b/wirelog/meson.build index db6190d1..7d3afbf8 100644 --- a/wirelog/meson.build +++ b/wirelog/meson.build @@ -40,6 +40,9 @@ wirelog_arena_src = files('arena/arena.c', ) # Select threading backend: C11 threads.h > POSIX pthreads > Windows MSVC # C11 threads.h detection uses cc.links() to verify actual support, # not just header presence (Apple Clang ships no threads.h). +# +# -Dthreads=posix forces POSIX pthreads (required for TSan, which only +# intercepts pthread calls, not C11 thread functions). c11_threads_test = ''' #include static int dummy(void *arg) { (void)arg; return 0; } @@ -50,7 +53,13 @@ int main(void) { return 0; } ''' -have_c11_threads = cc.links(c11_threads_test, name: 'C11 support') + +if threads_option == 'posix' + have_c11_threads = false + message(' Threading backend: POSIX pthreads (forced via -Dthreads=posix)') +else + have_c11_threads = cc.links(c11_threads_test, name: 'C11 support') +endif if have_c11_threads wirelog_thread_src = files('thread_c11.c', )