diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 7f615037..3c4d2dfa 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -212,3 +212,64 @@ jobs: echo "## Sanitizer Results: ${{ matrix.os }} / ${{ matrix.compiler }} (main monitor)" >> "$GITHUB_STEP_SUMMARY" echo '' >> "$GITHUB_STEP_SUMMARY" echo "Status: **${STATUS}** (non-blocking)" >> "$GITHUB_STEP_SUMMARY" + + # ========================================================================== + # Phase 4: TSan — Thread Sanitizer (parallel, non-blocking) + # Linux GCC + Clang only (TSan/ASan mutually exclusive) + # Monitors mutex safety in I/O adapter registry and workqueue. + # fail-fast: false — all configurations run to completion + # ========================================================================== + tsan: + name: TSan / ${{ matrix.os }} / ${{ matrix.compiler }} + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + # Linux - GCC with TSan + - os: ubuntu-latest + compiler: gcc + cc: gcc + + # Linux - Clang with TSan + - os: ubuntu-latest + compiler: clang + cc: clang + + steps: + - uses: actions/checkout@v5 + + - name: Install dependencies (Linux) + run: | + sudo apt-get update + sudo apt-get install -y meson ninja-build + if [ "${{ matrix.compiler }}" = "clang" ]; then + sudo apt-get install -y clang + fi + + - name: Configure with TSan + run: > + meson setup builddir-tsan + -Db_sanitize=thread + -Db_lundef=false + -Dtests=true + --buildtype=debug + env: + CC: ${{ matrix.cc }} + + - name: Build + run: meson compile -C builddir-tsan + + - name: Test + run: meson test -C builddir-tsan --print-errorlogs + env: + TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1 + + - name: Publish results + if: always() + run: | + STATUS="${{ job.status }}" + echo "## TSan Results: ${{ matrix.os }} / ${{ matrix.compiler }} (main monitor)" >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + echo "Status: **${STATUS}** (non-blocking)" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 67bba15a..d79ea7a8 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -143,3 +143,17 @@ jobs: env: ASAN_OPTIONS: abort_on_error=1:halt_on_error=1 UBSAN_OPTIONS: abort_on_error=1:halt_on_error=1:print_stacktrace=1 + + - name: Sanitizers / TSan + if: matrix.os == 'ubuntu-latest' + run: | + meson setup builddir-tsan \ + -Db_sanitize=thread \ + -Db_lundef=false \ + -Dtests=true \ + --buildtype=debug + meson compile -C builddir-tsan + meson test -C builddir-tsan --print-errorlogs + env: + TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1 + CC: ${{ matrix.cc }} diff --git a/tests/meson.build b/tests/meson.build index 51cb205e..888269be 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -2451,3 +2451,47 @@ test_io_dispatch_exe = executable( install: false, ) test('io_dispatch', test_io_dispatch_exe) + +# ============================================================================ +# Concurrent I/O Adapter Registry Stress Test (#459) +# Exercises mutex safety under TSan: concurrent find, register/unregister, +# and mixed readers+writers. Linux/macOS only (pthreads; skipped on Windows). +# ============================================================================ + +test_io_adapter_concurrent_exe = executable( + 'test_io_adapter_concurrent', + files('test_io_adapter_concurrent.c', + '../wirelog/io/io_adapter.c', + '../wirelog/io/csv_adapter.c', + '../wirelog/io/csv_reader.c', + '../wirelog/io/io_ctx.c', + '../wirelog/intern.c'), + thread_src, + include_directories: [wirelog_inc, wirelog_src_inc], + c_args: ['-DWIRELOG_BUILDING'], + dependencies: [threads_dep], + install: false, +) +test('io_adapter_concurrent', test_io_adapter_concurrent_exe) + +# ============================================================================ +# Mock Adapter Error-Path Tests for ASan (#459) +# Exercises NULL inputs, boundary scheme lengths, registry-full, and +# error-string clearing. ASan catches out-of-bounds writes on scheme copy. +# ============================================================================ + +test_io_adapter_asan_exe = executable( + 'test_io_adapter_asan', + files('test_io_adapter_asan.c', + '../wirelog/io/io_adapter.c', + '../wirelog/io/csv_adapter.c', + '../wirelog/io/csv_reader.c', + '../wirelog/io/io_ctx.c', + '../wirelog/intern.c'), + thread_src, + include_directories: [wirelog_inc, wirelog_src_inc], + c_args: ['-DWIRELOG_BUILDING'], + dependencies: [threads_dep], + install: false, +) +test('io_adapter_asan', test_io_adapter_asan_exe) diff --git a/tests/test_io_adapter_asan.c b/tests/test_io_adapter_asan.c new file mode 100644 index 00000000..a3b39b24 --- /dev/null +++ b/tests/test_io_adapter_asan.c @@ -0,0 +1,273 @@ +/* + * test_io_adapter_asan.c - Mock adapter error-path tests for ASan + * + * Copyright (C) CleverPlant + * Licensed under LGPL-3.0 + * + * Exercises error and boundary paths in the adapter registry. + * Designed to surface memory errors under ASan (Issue #459): + * - NULL inputs to all three public API functions + * - Scheme at SCHEME_MAX_LEN-1 boundary (max safe length) + * - Scheme one byte over SCHEME_MAX_LEN (must be truncated safely) + * - Fill registry to capacity then verify "registry full" error + * - wl_io_last_error() valid after each error path + * + * Part of #459 (ASan + TSan CI gates). + */ + +#include "wirelog/io/io_adapter.h" + +#include +#include + +/* ======================================================================== */ +/* Test Harness */ +/* ======================================================================== */ + +#define TEST(name) do { printf(" [TEST] %-60s ", name); } while (0) +#define PASS() do { printf("PASS\n"); passed++; } while (0) +#define FAIL(msg) do { printf("FAIL: %s\n", msg); failed++; } while (0) + +static int passed = 0, failed = 0; + +/* ======================================================================== */ +/* Helpers */ +/* ======================================================================== */ + +/* Scheme that is exactly SCHEME_MAX_LEN-1 characters (63 chars + NUL). */ +#define SCHEME_MAX_LEN 64 +static char s_long_scheme[SCHEME_MAX_LEN]; /* 63 'a' chars + NUL */ + +/* Scheme one byte longer than allowed: 64 'b' chars + NUL. The registry + * must truncate or reject it without writing past its internal buffer. */ +static char s_overlong_scheme[SCHEME_MAX_LEN + 2]; /* 64 'b' + NUL */ + +static void +build_boundary_schemes(void) +{ + memset(s_long_scheme, 'a', SCHEME_MAX_LEN - 1); + s_long_scheme[SCHEME_MAX_LEN - 1] = '\0'; + + memset(s_overlong_scheme, 'b', SCHEME_MAX_LEN); + s_overlong_scheme[SCHEME_MAX_LEN] = '\0'; +} + +/* ======================================================================== */ +/* Tests */ +/* ======================================================================== */ + +static void +test_null_adapter(void) +{ + TEST("register NULL adapter returns -1"); + int rc = wl_io_register_adapter(NULL); + if (rc != -1) { + FAIL("expected -1"); return; + } + const char *err = wl_io_last_error(); + if (err && err[0] != '\0') PASS(); + else FAIL("expected non-empty error string after NULL adapter"); +} + +static void +test_null_scheme_register(void) +{ + TEST("register adapter with NULL scheme returns -1"); + wl_io_adapter_t a; + memset(&a, 0, sizeof(a)); + a.scheme = NULL; + a.abi_version = WL_IO_ABI_VERSION; + int rc = wl_io_register_adapter(&a); + if (rc != -1) { + FAIL("expected -1"); return; + } + const char *err = wl_io_last_error(); + if (err && err[0] != '\0') PASS(); + else FAIL("expected non-empty error string"); +} + +static void +test_null_find(void) +{ + TEST("find NULL scheme returns NULL"); + const wl_io_adapter_t *found = wl_io_find_adapter(NULL); + if (found == NULL) PASS(); + else FAIL("expected NULL for NULL scheme"); +} + +static void +test_null_unregister(void) +{ + TEST("unregister NULL scheme returns -1"); + int rc = wl_io_unregister_adapter(NULL); + if (rc != -1) { + FAIL("expected -1"); return; + } + const char *err = wl_io_last_error(); + if (err && err[0] != '\0') PASS(); + else FAIL("expected non-empty error string"); +} + +static void +test_abi_mismatch(void) +{ + TEST("register adapter with wrong ABI version returns -1"); + wl_io_adapter_t a; + memset(&a, 0, sizeof(a)); + a.scheme = "mock_bad_abi2"; + a.abi_version = WL_IO_ABI_VERSION + 99u; + int rc = wl_io_register_adapter(&a); + if (rc == -1) PASS(); + else FAIL("expected -1 for ABI mismatch"); +} + +static void +test_boundary_scheme_length(void) +{ + TEST("register/find/unregister scheme at max length boundary"); + wl_io_adapter_t a; + memset(&a, 0, sizeof(a)); + a.scheme = s_long_scheme; /* 63 chars */ + a.abi_version = WL_IO_ABI_VERSION; + + int rc = wl_io_register_adapter(&a); + if (rc != 0) { + FAIL("register failed for max-length scheme"); return; + } + + const wl_io_adapter_t *found = wl_io_find_adapter(s_long_scheme); + if (found == NULL) { + FAIL("find failed for max-length scheme"); return; + } + + rc = wl_io_unregister_adapter(s_long_scheme); + if (rc == 0) PASS(); + else FAIL("unregister failed for max-length scheme"); +} + +static void +test_overlong_scheme_safe(void) +{ + TEST("register scheme over max length is safe (no buffer overrun)"); + /* s_overlong_scheme is 64 'b' chars + NUL. The registry copies with + * strncpy(dst, src, SCHEME_MAX_LEN-1) so it will be truncated to 63 'b' + * chars. We just verify the call does not crash (ASan would detect any + * out-of-bounds write). */ + wl_io_adapter_t a; + memset(&a, 0, sizeof(a)); + a.scheme = s_overlong_scheme; + a.abi_version = WL_IO_ABI_VERSION; + + /* May succeed or fail (truncated scheme might collide or not), but must + * not crash or write out of bounds. */ + int rc = wl_io_register_adapter(&a); + if (rc == 0) { + /* Clean up: look up by first SCHEME_MAX_LEN-1 chars */ + char truncated[SCHEME_MAX_LEN]; + memset(truncated, 'b', SCHEME_MAX_LEN - 1); + truncated[SCHEME_MAX_LEN - 1] = '\0'; + wl_io_unregister_adapter(truncated); + } + /* If rc == -1, that's also fine (duplicate or other error). */ + PASS(); +} + +static void +test_registry_full(void) +{ + TEST("register past WL_IO_MAX_ADAPTERS returns -1 with error"); + + /* csv is already registered (slot 0). Register adapters to fill the + * remaining 31 slots, then attempt one more. */ + wl_io_adapter_t adapters[WL_IO_MAX_ADAPTERS]; + char schemes[WL_IO_MAX_ADAPTERS][16]; + int registered = 0; + + for (int i = 0; i < WL_IO_MAX_ADAPTERS - 1; i++) { + snprintf(schemes[i], sizeof(schemes[i]), "fill_%d", i); + memset(&adapters[i], 0, sizeof(adapters[i])); + adapters[i].scheme = schemes[i]; + adapters[i].abi_version = WL_IO_ABI_VERSION; + if (wl_io_register_adapter(&adapters[i]) == 0) + registered++; + } + + /* Try to register one more beyond capacity */ + wl_io_adapter_t overflow; + memset(&overflow, 0, sizeof(overflow)); + overflow.scheme = "overflow_scheme"; + overflow.abi_version = WL_IO_ABI_VERSION; + int rc = wl_io_register_adapter(&overflow); + + /* Cleanup: unregister all fill_ adapters we registered */ + for (int i = 0; i < WL_IO_MAX_ADAPTERS - 1; i++) { + if (adapters[i].scheme) + wl_io_unregister_adapter(schemes[i]); + } + + if (rc == -1 && registered == WL_IO_MAX_ADAPTERS - 1) PASS(); + else if (rc == -1) { + /* Registry may have been partially filled by prior test state */ + PASS(); + } else { + FAIL("expected -1 when registry is full"); + } +} + +static void +test_unregister_nonexistent(void) +{ + TEST("unregister non-existent scheme returns -1"); + int rc = wl_io_unregister_adapter("scheme_that_was_never_registered"); + if (rc == -1) PASS(); + else FAIL("expected -1 for unknown scheme"); +} + +static void +test_error_string_after_success(void) +{ + TEST("wl_io_last_error() is empty after successful operation"); + wl_io_adapter_t a; + memset(&a, 0, sizeof(a)); + a.scheme = "mock_err_clear"; + a.abi_version = WL_IO_ABI_VERSION; + + int rc = wl_io_register_adapter(&a); + if (rc != 0) { + FAIL("register failed unexpectedly"); return; + } + + const char *err = wl_io_last_error(); + int ok = (err == NULL || err[0] == '\0'); + + wl_io_unregister_adapter("mock_err_clear"); + + if (ok) PASS(); + else FAIL("expected empty error string after success"); +} + +/* ======================================================================== */ +/* Main */ +/* ======================================================================== */ + +int +main(void) +{ + printf("=== test_io_adapter_asan (Issue #459) ===\n"); + + build_boundary_schemes(); + + test_null_adapter(); + test_null_scheme_register(); + test_null_find(); + test_null_unregister(); + test_abi_mismatch(); + test_boundary_scheme_length(); + test_overlong_scheme_safe(); + test_registry_full(); + test_unregister_nonexistent(); + test_error_string_after_success(); + + printf("=== Results: %d passed, %d failed ===\n", passed, failed); + return failed > 0 ? 1 : 0; +} diff --git a/tests/test_io_adapter_concurrent.c b/tests/test_io_adapter_concurrent.c new file mode 100644 index 00000000..5da120be --- /dev/null +++ b/tests/test_io_adapter_concurrent.c @@ -0,0 +1,212 @@ +/* + * test_io_adapter_concurrent.c - Concurrent I/O adapter registry stress test + * + * Copyright (C) CleverPlant + * Licensed under LGPL-3.0 + * + * Exercises the mutex-guarded adapter registry under concurrent load. + * Designed to expose data races under TSan (Issue #459). + * + * Tests: + * 1. Concurrent find: N threads repeatedly call wl_io_find_adapter + * 2. Concurrent register/unregister: N threads each own a unique scheme + * 3. Mixed readers + writers: find and register running simultaneously + * + * Part of #459 (ASan + TSan CI gates). + */ + +#include "wirelog/io/io_adapter.h" + +#include +#include + +/* ======================================================================== */ +/* Test Harness */ +/* ======================================================================== */ + +#define TEST(name) do { printf(" [TEST] %-60s ", name); } while (0) +#define PASS() do { printf("PASS\n"); passed++; } while (0) +#define FAIL(msg) do { printf("FAIL: %s\n", msg); failed++; } while (0) + +static int passed = 0, failed = 0; + +/* ======================================================================== */ +/* Concurrent Tests (POSIX only; skipped on Windows) */ +/* ======================================================================== */ + +#ifndef _WIN32 + +#include + +#define NTHREADS 8 +#define FIND_ITERS 2000 + +/* ---- Test 1: concurrent find ------------------------------------------- */ + +static void * +find_worker(void *arg) +{ + (void)arg; + for (int i = 0; i < FIND_ITERS; i++) { + /* csv is the built-in; must always be findable */ + const wl_io_adapter_t *a = wl_io_find_adapter("csv"); + (void)a; + } + return NULL; +} + +static void +test_concurrent_find(void) +{ + TEST("concurrent find (csv built-in, 8 threads x 2000 iters)"); + + pthread_t threads[NTHREADS]; + for (int i = 0; i < NTHREADS; i++) + pthread_create(&threads[i], NULL, find_worker, NULL); + for (int i = 0; i < NTHREADS; i++) + pthread_join(threads[i], NULL); + + /* After concurrent reads, csv must still be findable */ + const wl_io_adapter_t *a = wl_io_find_adapter("csv"); + if (a != NULL) PASS(); + else FAIL("csv not found after concurrent reads"); +} + +/* ---- Test 2: concurrent register/unregister ----------------------------- */ + +typedef struct { + char scheme[32]; + wl_io_adapter_t adapter; + int result; /* 0 = ok, 1 = error */ +} reg_arg_t; + +static void * +reg_unreg_worker(void *arg) +{ + reg_arg_t *a = (reg_arg_t *)arg; + + /* Each thread owns a unique scheme; register then unregister FIND_ITERS + * times. The registry has 32 slots total; with 8 threads, each holding + * at most 1 slot at a time, we stay well within capacity. */ + for (int i = 0; i < FIND_ITERS; i++) { + int rc = wl_io_register_adapter(&a->adapter); + if (rc != 0) { + a->result = 1; + return NULL; + } + rc = wl_io_unregister_adapter(a->scheme); + if (rc != 0) { + a->result = 1; + return NULL; + } + } + a->result = 0; + return NULL; +} + +static void +test_concurrent_register_unregister(void) +{ + TEST("concurrent register/unregister (8 threads x 2000 iters each)"); + + pthread_t threads[NTHREADS]; + reg_arg_t args[NTHREADS]; + + for (int i = 0; i < NTHREADS; i++) { + snprintf(args[i].scheme, sizeof(args[i].scheme), "conc_%d", i); + memset(&args[i].adapter, 0, sizeof(args[i].adapter)); + 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]); + } + for (int i = 0; i < NTHREADS; i++) + pthread_join(threads[i], NULL); + + int any_failed = 0; + for (int i = 0; i < NTHREADS; i++) { + if (args[i].result != 0) { + any_failed = 1; + break; + } + } + if (!any_failed) PASS(); + else FAIL("register/unregister cycle failed in one or more threads"); +} + +/* ---- Test 3: mixed readers + writers ------------------------------------ */ + +static void * +mixed_find_worker(void *arg) +{ + (void)arg; + for (int i = 0; i < FIND_ITERS; i++) { + const wl_io_adapter_t *a = wl_io_find_adapter("csv"); + (void)a; + a = wl_io_find_adapter("nonexistent_scheme_xyz"); + (void)a; + } + return NULL; +} + +static void * +mixed_reg_worker(void *arg) +{ + reg_arg_t *a = (reg_arg_t *)arg; + for (int i = 0; i < FIND_ITERS; i++) { + wl_io_register_adapter(&a->adapter); + wl_io_unregister_adapter(a->scheme); + } + return NULL; +} + +static void +test_mixed_readers_writers(void) +{ + TEST("mixed readers + writers (4 find + 4 reg/unreg threads)"); + + pthread_t readers[4]; + pthread_t writers[4]; + reg_arg_t wargs[4]; + + for (int i = 0; i < 4; i++) { + snprintf(wargs[i].scheme, sizeof(wargs[i].scheme), "mixed_%d", i); + 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]); + } + for (int i = 0; i < 4; i++) { + pthread_join(readers[i], NULL); + pthread_join(writers[i], NULL); + } + + /* Registry must still be consistent: csv findable, no dangling entries */ + const wl_io_adapter_t *a = wl_io_find_adapter("csv"); + if (a != NULL) PASS(); + else FAIL("csv not found after mixed concurrent access"); +} + +#endif /* !_WIN32 */ + +/* ======================================================================== */ +/* Main */ +/* ======================================================================== */ + +int +main(void) +{ + printf("=== test_io_adapter_concurrent (Issue #459) ===\n"); + +#ifndef _WIN32 + test_concurrent_find(); + test_concurrent_register_unregister(); + test_mixed_readers_writers(); +#else + printf(" [SKIP] concurrent tests not supported on Windows\n"); +#endif + + printf("=== Results: %d passed, %d failed ===\n", passed, failed); + return failed > 0 ? 1 : 0; +} diff --git a/wirelog/io/io_adapter.c b/wirelog/io/io_adapter.c index 5ba337b6..8e3c269e 100644 --- a/wirelog/io/io_adapter.c +++ b/wirelog/io/io_adapter.c @@ -71,10 +71,16 @@ 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 volatile flag + InterlockedCompareExchange for one-shot init. */ + * use a two-phase flag + InterlockedCompareExchange for one-shot init. + * + * 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) static mutex_t s_mutex; -static volatile long s_mutex_ready; /* 0=uninit, 1=ready */ +/* 0 = uninitialised, 1 = initialisation in progress, 2 = ready */ +static volatile long s_mutex_init; #else static mutex_t s_mutex = { PTHREAD_MUTEX_INITIALIZER }; #endif @@ -90,14 +96,21 @@ extern const wl_io_adapter_t wl_csv_adapter; static void ensure_builtins(void) { - /* On Windows, CRITICAL_SECTION requires explicit init. Use a volatile - * flag with InterlockedCompareExchange for the one-shot mutex_init. */ + /* On Windows, CRITICAL_SECTION requires explicit init. + * 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 (!s_mutex_ready) { - if (InterlockedCompareExchange(&s_mutex_ready, 1, 0) == 0) + if (InterlockedCompareExchange(&s_mutex_init, 2, 2) != 2) { + if (InterlockedCompareExchange(&s_mutex_init, 1, 0) == 0) { mutex_init(&s_mutex); - /* Spin until the initializer thread finishes mutex_init. The flag - * is set before mutex_init returns above, so the mutex is ready. */ + InterlockedExchange(&s_mutex_init, 2); + } + /* Spin until the winning thread completes mutex_init. */ + while (InterlockedCompareExchange(&s_mutex_init, 2, 2) != 2) + SwitchToThread(); } #endif /* Use the mutex (statically initialized on POSIX, dynamically on diff --git a/wirelog/io/io_adapter.h b/wirelog/io/io_adapter.h index d7627763..3cc37c91 100644 --- a/wirelog/io/io_adapter.h +++ b/wirelog/io/io_adapter.h @@ -84,6 +84,38 @@ typedef struct wl_io_adapter { #define WL_IO_USED #endif +/* + * Adapter Lifetime Contract + * ------------------------- + * The wl_io_adapter_t pointer passed to wl_io_register_adapter() MUST remain + * valid (i.e. the pointed-to struct must not be freed or modified) until the + * corresponding wl_io_unregister_adapter() call or process exit. The registry + * stores the raw pointer and returns it verbatim from wl_io_find_adapter(). + * + * The scheme string within the adapter struct is copied into an internal + * fixed-size buffer (SCHEME_MAX_LEN-1 characters) at registration time, so + * the caller's scheme pointer may be freed or reused after register returns. + * + * Thread Safety + * ------------- + * All three public API functions (register, unregister, find) are thread-safe. + * They acquire a process-global mutex for the duration of each operation. + * TSan CI gate (Issue #459) validates this invariant on every PR. + * + * Error Reporting + * --------------- + * On failure, functions return -1 and record a human-readable reason in a + * thread-local buffer retrievable via wl_io_last_error(). On success the + * error buffer is cleared. The returned pointer is valid until the next call + * on the same thread. + * + * Return values: + * wl_io_register_adapter 0 on success, -1 on error (NULL input, ABI + * mismatch, duplicate scheme, or registry full) + * wl_io_unregister_adapter 0 on success, -1 if scheme is not registered + * wl_io_find_adapter pointer to adapter, or NULL if not found + */ + WL_PUBLIC int wl_io_register_adapter(const wl_io_adapter_t *adapter) WL_IO_USED; WL_PUBLIC int wl_io_unregister_adapter(const char *scheme) WL_IO_USED;