diff --git a/.github/workflows/abi-compat.yml b/.github/workflows/abi-compat.yml index 76674ea..2e61ec4 100644 --- a/.github/workflows/abi-compat.yml +++ b/.github/workflows/abi-compat.yml @@ -30,6 +30,14 @@ jobs: fetch-depth: 0 fetch-tags: true + - name: Setup MSVC (Developer Command Prompt) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + - name: Install Ninja [Windows] + if: runner.os == 'Windows' + run: choco install ninja --no-progress --yes + - name: Fetch tags (ensure up-to-date) run: git fetch --tags --force @@ -78,7 +86,7 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - cmake -B build -G "Visual Studio 17 2022" -A x64 -DTHREADSCHEDULE_RUNTIME=ON -DCMAKE_INSTALL_PREFIX="$env:RUNNER_TEMP/ts-install" + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DTHREADSCHEDULE_RUNTIME=ON -DCMAKE_INSTALL_PREFIX="$env:RUNNER_TEMP/ts-install" - name: Build (current) [Windows] if: runner.os == 'Windows' shell: pwsh @@ -110,7 +118,7 @@ jobs: shell: pwsh working-directory: integration_tests/runtime_abi_compat run: | - cmake -B build -G "Visual Studio 17 2022" -A x64 -DTHREADSCHEDULE_RUNTIME=ON -DRUNTIME_ABI_OLD_OFFSET=${{ matrix.offset }} -DCMAKE_PREFIX_PATH="$env:RUNNER_TEMP/ts-install" + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DTHREADSCHEDULE_RUNTIME=ON -DRUNTIME_ABI_OLD_OFFSET=${{ matrix.offset }} -DCMAKE_PREFIX_PATH="$env:RUNNER_TEMP/ts-install" - name: Build integration test [Windows] if: runner.os == 'Windows' shell: pwsh @@ -123,4 +131,3 @@ jobs: run: ctest --test-dir build -C Release --output-on-failure continue-on-error: ${{ inputs.expected_abi_break == true || steps.major_flag.outputs.changed == 'true' }} - diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b67a7..7e8503b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,84 @@ # Changelog +## v2.4.0 + +> This release adds an explicit stable-ABI subset for shared-runtime / DSO +> boundaries, introduces a migration path with deprecations before hard +> enforcement, and expands ABI-focused regression coverage. + +### ABI Stability + +- **New opt-in stable ABI subset for runtime boundaries** -- the shared-runtime + build now exposes `threadschedule::abi::*` helpers with opaque + `registry_handle`, POD-style `thread_info_view`, stable status codes, and a + dedicated `abi::AutoRegisterCurrentThread` path for cross-DSO integration. + (`abi.hpp`, `runtime_registry.cpp`) + +- **Compile-time ABI markers and export validation** -- the new + `threadschedule::abi::is_abi_stable_v` trait and + `THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(...)` macro let library and + downstream code mark and enforce signatures that are safe to export across a + stable ABI boundary. (`abi.hpp`) + +- **Stable-ABI build modes for migration and enforcement** -- CMake now + provides `THREADSCHEDULE_STABLE_ABI=ON` for migration builds and + `THREADSCHEDULE_STABLE_ABI_STRICT=ON` for hard enforcement. In runtime mode, + the strict path rejects ABI-unsafe entry points such as + `registry()`, `set_external_registry(ThreadRegistry*)`, and the legacy + `AutoRegisterCurrentThread` constructors at compile time. (`CMakeLists.txt`, + `thread_registry.hpp`) + +### Compatibility + +- **Deprecation-first migration path for legacy runtime APIs** -- when + `THREADSCHEDULE_STABLE_ABI=ON` is enabled without strict mode, the existing + runtime-facing C++ helpers remain available but are marked deprecated with + guidance toward `threadschedule::abi::*`. This keeps default builds + source-compatible while making ABI-unsafe usage visible before it becomes a + hard error. (`export.hpp`, `thread_registry.hpp`) + +- **Runtime internals now route through non-deprecated helpers** -- internal + registry access was split into dedicated detail helpers so ThreadSchedule's + own headers and runtime implementation do not trip the new deprecation path + during normal compilation. (`thread_registry.hpp`, `runtime_registry.cpp`, + `chaos.hpp`) + +### Tests + +- **New stable-ABI regression coverage** -- added dedicated tests for the new + ABI surface plus compile-time checks that confirm: + stable handles are accepted, `ThreadRegistry*` exports are rejected, and + runtime `registry()` usage transitions from deprecation in migration mode to + hard failure in strict mode. (`tests/abi_test.cpp`, `tests/CMakeLists.txt`) + +- **Cross-standard runtime ABI coverage was strengthened** -- the integration + test now explicitly mixes an older C++17-built dependency with C++23-built + current components to keep the mixed-standard runtime scenario visible in CI + and local release validation. (`integration_tests/runtime_abi_compat/*`) + +## v2.3.1 + +> This release focuses on ABI hardening for mixed-standard consumers of the +> shared runtime. + +### ABI / Runtime Fixes + +- **`threadschedule::expected` is now always the library-owned type** -- the + public `expected` alias no longer flips over to `std::expected` in C++23+. + This stabilizes exported signatures across consumers compiled with different + language modes and avoids cross-DSO ABI mismatches when an intermediate + library exposes ThreadSchedule result types. (`expected.hpp`) + +- **Runtime visibility and consumer defines were tightened** -- the runtime + target now consistently exports default-visible symbols and propagates the + `THREADSCHEDULE_RUNTIME` define so consumers call into the shared runtime + instead of accidentally instantiating a separate header-only registry. + (`thread_registry.hpp`, `CMakeLists.txt`) + +- **Packaging/tooling fixes for runtime consumers** -- Conan/CMake packaging + paths were adjusted so runtime-enabled consumers resolve the intended build + configuration more reliably. (`conanfile.py`, `CMakeLists.txt`) + ## v2.3.0 > This release adds an opt-in GCC 16/C++26 reflection surface, modernizes @@ -484,6 +563,18 @@ auto futures = pool.submit_range(tasks.begin(), tasks.end()); auto futures = pool.submit_batch(tasks.begin(), tasks.end()); ``` +## v1.4.3 + +- Docs: clarified scheduled-task storage and dispatch-order edge cases in + `scheduled_pool.hpp`, especially around queueing semantics and execution + ordering guarantees for scheduled workloads. + +## v1.4.2 + +- Docs: expanded and restructured documentation across multiple public headers + to better explain thread wrappers, registries, pools, and scheduling-related + APIs without changing library behaviour. + ## v1.4.1 - Fix: `*WrapperReg` types (`ThreadWrapperReg`, `JThreadWrapperReg`, diff --git a/CMakeLists.txt b/CMakeLists.txt index 5256cec..9bf4aaa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,10 +62,16 @@ option(THREADSCHEDULE_BUILD_TESTS "Build tests" OFF) option(THREADSCHEDULE_BUILD_BENCHMARKS "Build benchmarks" OFF) option(THREADSCHEDULE_INSTALL "Generate install target" ${THREADSCHEDULE_IS_TOPLEVEL_PROJECT}) option(THREADSCHEDULE_RUNTIME "Build shared runtime for global registry (non header-only)" ON) +option(THREADSCHEDULE_STABLE_ABI "Expose the stable ABI subset helpers" OFF) +option(THREADSCHEDULE_STABLE_ABI_STRICT "Reject non-stable runtime ABI usage at compile time" OFF) option(THREADSCHEDULE_MODULE "Build C++20 module target (requires CMake >= 3.28 and C++20+)" OFF) option(THREADSCHEDULE_BUILD_DOCS "Build API documentation with Doxygen" ${THREADSCHEDULE_IS_TOPLEVEL_PROJECT}) option(THREADSCHEDULE_ENABLE_REFLECTION "Enable GCC 16 C++26 reflection APIs when supported" OFF) +if(THREADSCHEDULE_STABLE_ABI_STRICT AND NOT THREADSCHEDULE_STABLE_ABI) + set(THREADSCHEDULE_STABLE_ABI ON) +endif() + # CPM support (optional, download if building tests or benchmarks) if(THREADSCHEDULE_BUILD_TESTS OR THREADSCHEDULE_BUILD_BENCHMARKS) # Download CPM.cmake if not already available @@ -176,6 +182,14 @@ target_include_directories(ThreadSchedule INTERFACE $ ) +if(THREADSCHEDULE_STABLE_ABI) + target_compile_definitions(ThreadSchedule INTERFACE THREADSCHEDULE_STABLE_ABI=1) +endif() + +if(THREADSCHEDULE_STABLE_ABI_STRICT) + target_compile_definitions(ThreadSchedule INTERFACE THREADSCHEDULE_STABLE_ABI_STRICT=1) +endif() + # Link libraries target_link_libraries(ThreadSchedule INTERFACE Threads::Threads) @@ -249,13 +263,20 @@ if(THREADSCHEDULE_RUNTIME) add_library(ThreadScheduleRuntime SHARED src/runtime_registry.cpp ) - target_compile_definitions(ThreadScheduleRuntime PRIVATE THREADSCHEDULE_EXPORTS THREADSCHEDULE_RUNTIME) + target_compile_definitions(ThreadScheduleRuntime PRIVATE THREADSCHEDULE_EXPORTS THREADSCHEDULE_RUNTIME + THREADSCHEDULE_INTERNAL_RUNTIME_BUILD=1) if(THREADSCHEDULE_HAS_REFLECTION) target_compile_definitions(ThreadScheduleRuntime PUBLIC THREADSCHEDULE_HAS_REFLECTION=1) target_compile_options(ThreadScheduleRuntime PUBLIC $<$:-freflection>) endif() # Propagate the THREADSCHEDULE_RUNTIME define to consumers so headers call into the DLL target_compile_definitions(ThreadScheduleRuntime INTERFACE THREADSCHEDULE_RUNTIME) + if(THREADSCHEDULE_STABLE_ABI) + target_compile_definitions(ThreadScheduleRuntime PUBLIC THREADSCHEDULE_STABLE_ABI=1) + endif() + if(THREADSCHEDULE_STABLE_ABI_STRICT) + target_compile_definitions(ThreadScheduleRuntime PUBLIC THREADSCHEDULE_STABLE_ABI_STRICT=1) + endif() target_include_directories(ThreadScheduleRuntime PUBLIC $ diff --git a/README.md b/README.md index 6254f84..c6984c2 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ or with optional **shared runtime** for multi-DSO applications. feature detection and optimization - **C++20 Modules**: Optional `import threadschedule;` support (C++20+) - **Header-Only or Shared Runtime**: Choose based on your needs +- **Stable ABI Subset**: `threadschedule::abi::*` helpers for runtime-backed + DSO/plugin boundaries across mixed language modes - **Enhanced Wrappers**: Extend `std::thread`, `std::jthread`, and `pthread` with powerful features - **Non-owning Views**: Zero-overhead views to configure existing threads or @@ -102,7 +104,7 @@ when upgrading from v1.x. - **[Integration Guide](docs/INTEGRATION.md)** - CMake, Conan, FetchContent, system installation - **[Thread Registry Guide](docs/REGISTRY.md)** - Process-wide thread control - and multi-DSO patterns + and multi-DSO / stable-ABI patterns - **[Scheduled Tasks Guide](docs/SCHEDULED_TASKS.md)** - Timer and periodic task scheduling - **[Error Handling Guide](docs/ERROR_HANDLING.md)** - Exception handling with @@ -415,8 +417,17 @@ int main() { ``` **For multi-DSO applications:** Use the shared runtime option -(`THREADSCHEDULE_RUNTIME=ON`) to ensure a single process-wide registry. See -[docs/REGISTRY.md](docs/REGISTRY.md) for detailed patterns. +(`THREADSCHEDULE_RUNTIME=ON`) to ensure a single process-wide registry. + +If your app or plugin ABI crosses shared-library boundaries, prefer the stable +ABI subset in `threadschedule::abi::*` instead of exporting +`ThreadRegistry`, `RegisteredThreadInfo`, or `AutoRegisterCurrentThread` +directly in your own ABI. Enabling `THREADSCHEDULE_STABLE_ABI=ON` adds +deprecation warnings for runtime-backed APIs that are unsafe to expose across +those boundaries, including mixed builds such as one DSO compiled as C++17 and +another as C++23, and `THREADSCHEDULE_STABLE_ABI_STRICT=ON` turns those uses +into compile errors. See [docs/REGISTRY.md](docs/REGISTRY.md) and +[docs/CMAKE_REFERENCE.md](docs/CMAKE_REFERENCE.md) for the migration details. Notes: diff --git a/VERSION b/VERSION index ccbccc3..197c4d5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.0 +2.4.0 diff --git a/docs/CMAKE_REFERENCE.md b/docs/CMAKE_REFERENCE.md index 57e1060..de504bf 100644 --- a/docs/CMAKE_REFERENCE.md +++ b/docs/CMAKE_REFERENCE.md @@ -8,6 +8,8 @@ | `THREADSCHEDULE_BUILD_TESTS` | BOOL | OFF | Build unit tests | | `THREADSCHEDULE_BUILD_BENCHMARKS` | BOOL | OFF | Build benchmarks (downloads Google Benchmark) | | `THREADSCHEDULE_RUNTIME` | BOOL | OFF | Build shared runtime library for process-wide registry | +| `THREADSCHEDULE_STABLE_ABI` | BOOL | OFF | Expose the stable ABI subset and deprecate runtime-backed APIs that are unsafe across DSO boundaries | +| `THREADSCHEDULE_STABLE_ABI_STRICT` | BOOL | OFF | Reject ABI-unsafe runtime API usage at compile time; implies `THREADSCHEDULE_STABLE_ABI` | | `THREADSCHEDULE_ENABLE_REFLECTION` | BOOL | OFF | Enable GCC 16+ C++26 reflection APIs and reflection-backed registry queries when supported | | `THREADSCHEDULE_INSTALL` | BOOL | ON (main project)
OFF (subdirectory) | Generate install targets | @@ -115,6 +117,20 @@ target_link_libraries(my_app PRIVATE **Result**: A shared runtime library (`libthreadschedule.so` / `threadschedule.dll`) is built. All components share a single process-wide registry instance. +### With Shared Runtime and Stable ABI Checks +```cmake +set(THREADSCHEDULE_RUNTIME ON) +set(THREADSCHEDULE_STABLE_ABI ON) +# or: set(THREADSCHEDULE_STABLE_ABI_STRICT ON) +add_subdirectory(external/ThreadSchedule) +``` + +**Result**: The shared runtime exports the `threadschedule::abi::*` helper +surface for DSO boundaries. In migration mode, legacy runtime APIs such as +`registry()` and `AutoRegisterCurrentThread` emit deprecation warnings. In +strict mode, those runtime-backed APIs become compile-time errors outside the +runtime implementation itself. + ### Development Build (All Features) ```cmake set(THREADSCHEDULE_BUILD_EXAMPLES ON) @@ -167,7 +183,8 @@ Optional shared runtime target for process-wide registry. Properties: - **Type**: SHARED library (DLL/SO) - **Availability**: Only when `THREADSCHEDULE_RUNTIME=ON` - **Include directories**: `include/` -- **Exports**: `registry()` and `set_external_registry()` functions +- **Exports**: `registry()` / `set_external_registry()` plus the + `threadschedule::abi::*` runtime helpers - **Use case**: Multi-DSO applications requiring single registry instance ### Usage @@ -184,6 +201,15 @@ target_link_libraries(your_target PRIVATE **Note**: When using the runtime library, all DSOs (libraries and executables) in your application must link against `ThreadSchedule::Runtime` to share the same registry instance. +**ABI guidance**: + +- Use `threadschedule::abi::*` for plugin/DSO ABI boundaries. +- Treat `registry()`, `set_external_registry(ThreadRegistry*)`, and + `AutoRegisterCurrentThread` as migration-only runtime APIs once + `THREADSCHEDULE_STABLE_ABI` is enabled. +- Use `THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(...)` in exported headers to + static-assert that boundary signatures only use stable ABI types. + ## Platform-Specific Behavior ### Linux @@ -301,6 +327,19 @@ endif() 1. Use `THREADSCHEDULE_RUNTIME=ON` to build a shared runtime (recommended for multi-DSO) 2. Explicitly inject the registry into each DSO via `set_external_registry()` +### Deprecation Warnings for `registry()` in Runtime Mode +**Symptom**: Enabling `THREADSCHEDULE_STABLE_ABI=ON` produces deprecation +warnings for `registry()`, `set_external_registry(ThreadRegistry*)`, or +`AutoRegisterCurrentThread`. + +**Cause**: Those C++ runtime entrypoints are still usable inside a single build, +but they are not part of the stable ThreadSchedule ABI subset for exported +shared-library boundaries. + +**Solution**: Keep them for internal-only code during migration, but move any +exported plugin/DSO boundary to `threadschedule::abi::*`. When the migration is +complete, switch to `THREADSCHEDULE_STABLE_ABI_STRICT=ON`. + ## Advanced Configuration ### Custom Compiler Flags (Top-Level Project Only) @@ -354,11 +393,9 @@ FetchContent_MakeAvailable(ThreadSchedule) ``` ### Conan -See [conanfile.py](../conanfile.py) for Conan package definition. - -```bash -conan create . --build=missing -``` +ThreadSchedule does not currently ship a maintained in-tree Conan recipe. +Use FetchContent/CPM directly, or keep a project-local Conan recipe that pulls +the tagged Git release you want to consume. ## Best Practices @@ -369,7 +406,9 @@ conan create . --build=missing 5. **Pin to specific version** (tag) in production 6. **Test with your target C++ standard** before deploying 7. **For multi-DSO applications**: Use `THREADSCHEDULE_RUNTIME=ON` to ensure single registry -8. **On Windows with runtime**: Copy DLLs to executable directory or use install(RUNTIME_DEPENDENCY_SET) +8. **For exported plugin/DSO ABIs**: Enable `THREADSCHEDULE_STABLE_ABI=ON` early, and move public registry-facing boundaries to `threadschedule::abi::*` +9. **Validate exported signatures**: Use `THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(...)` in public headers for registry-related ABI boundaries +10. **On Windows with runtime**: Copy DLLs to executable directory or use install(RUNTIME_DEPENDENCY_SET) ## Example Project Structure diff --git a/docs/REGISTRY.md b/docs/REGISTRY.md index ca2fe27..f28c00e 100644 --- a/docs/REGISTRY.md +++ b/docs/REGISTRY.md @@ -250,6 +250,56 @@ graph TD - If components statically include ThreadSchedule: Use `set_external_registry(&appRegistry)` in `main()` and register threads to that instance everywhere. - If isolated registries are desired for components: Each component uses its own `ThreadRegistry`, and the app merges them using `CompositeThreadRegistry`. +### Stable ABI subset for DSO and plugin boundaries + +If a shared library, plugin, or intermediate wrapper library exports +ThreadSchedule-related types in its own ABI, treat the normal registry API as a +source-level API, not as a stable binary contract. This matters especially when +different components may be built in different language modes, for example one +DSO as C++17 and another as C++23. + +For the full rationale, byte-level layout discussion, mixed-standard Conan +failure walkthrough, and `expected` mismatch example, see +[STABLE_ABI.md](./STABLE_ABI.md). + +Use the `threadschedule::abi::*` surface when all of these are true: + +- components cross a shared-library boundary +- components may be built with different language modes or standard library + configurations +- the boundary needs registry access or thread auto-registration + +The stable ABI subset is intentionally small and C-like: + +- `threadschedule::abi::registry_handle` +- `threadschedule::abi::string_ref` +- `threadschedule::abi::status` / `status_code` +- `threadschedule::abi::thread_info_view` +- `threadschedule::abi::thread_info_callback` +- `threadschedule::abi::create_registry()`, `destroy_registry(...)`, + `current_registry()`, `set_external_registry(...)`, + `registry_for_each(...)`, `register_current_thread(...)`, + `unregister_current_thread(...)` +- `threadschedule::abi::AutoRegisterCurrentThread` + +Do not export these runtime-oriented C++ types as part of your own DSO ABI: + +- `ThreadRegistry` or `ThreadRegistry*` +- `RegisteredThreadInfo` +- `AutoRegisterCurrentThread` +- callbacks or virtual interfaces that embed those types, `std::string`, or + other STL-heavy registry payloads directly in the exported signature + +Build-time migration helpers: + +- `THREADSCHEDULE_RUNTIME=ON` enables the shared runtime that backs the stable + ABI entrypoints. +- `THREADSCHEDULE_STABLE_ABI=ON` keeps the legacy runtime API available but + marks `registry()`, `set_external_registry(ThreadRegistry*)`, and + `AutoRegisterCurrentThread` as deprecated for DSO boundaries. +- `THREADSCHEDULE_STABLE_ABI_STRICT=ON` rejects those legacy runtime entrypoints + at compile time and forces use of `threadschedule::abi::*`. + ### Header-only builds and multiple DSOs Because ThreadSchedule is header-only, each DSO that includes it may get its own internal `registry()` singleton. To obtain a unified process-wide view, use one of these patterns: @@ -318,11 +368,15 @@ target_link_libraries(your_dso PRIVATE ThreadSchedule::ThreadSchedule ThreadSche - Exported APIs (same as header-only), provided by the runtime: - `threadschedule::registry()` - returns the single process-wide registry instance - `threadschedule::set_external_registry(ThreadRegistry*)` - optionally redirect runtime to an app-owned instance + - `threadschedule::abi::*` helpers - stable handle/view/callback surface for + plugin and DSO boundaries Notes: - With `THREADSCHEDULE_RUNTIME=ON`, the header declares these functions and the `.so/.dll` provides the definitions. - This ensures all components in the process resolve to the same registry object as long as they link to the runtime. - You can still call `set_external_registry(&appReg)` early in `main()` to make the app’s instance authoritative. +- If your own shared-library ABI exposes ThreadSchedule-related registry access, + prefer `threadschedule::abi::*` over the legacy C++ runtime API. ### Examples @@ -612,5 +666,3 @@ All control functions return `expected`. Typical errors i - Linux cgroup helper (best-effort): - `cgroup_attach_tid("/sys/fs/cgroup/mygroup", e.tid)` attempts to write the TID into common cgroup files (`cgroup.threads`, `tasks`, `cgroup.procs`). - Requires appropriate privileges; returns `operation_not_permitted` on failure. - - diff --git a/docs/STABLE_ABI.md b/docs/STABLE_ABI.md new file mode 100644 index 0000000..45de9e6 --- /dev/null +++ b/docs/STABLE_ABI.md @@ -0,0 +1,508 @@ +# Stable ABI for DSO and Plugin Boundaries + +This guide explains when to use the `threadschedule::abi::*` surface, why the +normal C++ registry API is not a stable binary boundary, and what can go wrong +when richer C++ objects cross DSOs compiled under different assumptions. + +For the broader registry usage guide, see [REGISTRY.md](./REGISTRY.md). + +## When to use the stable ABI subset + +If a shared library, plugin, or intermediate wrapper library exports +ThreadSchedule-related types in its own ABI, treat the normal registry API as a +source-level API, not as a stable binary contract. This matters especially when +different components may be built in different language modes, for example one +DSO as C++17 and another as C++23. + +Use the `threadschedule::abi::*` surface when all of these are true: + +- components cross a shared-library boundary +- components may be built with different language modes or standard library + configurations +- the boundary needs registry access or thread auto-registration + +## Stable ABI subset + +The stable ABI subset is intentionally small and C-like: + +- `threadschedule::abi::registry_handle` +- `threadschedule::abi::string_ref` +- `threadschedule::abi::status` / `status_code` +- `threadschedule::abi::thread_info_view` +- `threadschedule::abi::thread_info_callback` +- `threadschedule::abi::create_registry()`, `destroy_registry(...)`, + `current_registry()`, `set_external_registry(...)`, + `registry_for_each(...)`, `register_current_thread(...)`, + `unregister_current_thread(...)` +- `threadschedule::abi::AutoRegisterCurrentThread` + +Do not export these runtime-oriented C++ types as part of your own DSO ABI: + +- `ThreadRegistry` or `ThreadRegistry*` +- `RegisteredThreadInfo` +- `AutoRegisterCurrentThread` +- callbacks or virtual interfaces that embed those types, `std::string`, or + other STL-heavy registry payloads directly in the exported signature + +## Build-time migration helpers + +- `THREADSCHEDULE_RUNTIME=ON` enables the shared runtime that backs the stable + ABI entrypoints. +- `THREADSCHEDULE_STABLE_ABI=ON` keeps the legacy runtime API available but + marks `registry()`, `set_external_registry(ThreadRegistry*)`, and + `AutoRegisterCurrentThread` as deprecated for DSO boundaries. +- `THREADSCHEDULE_STABLE_ABI_STRICT=ON` rejects those legacy runtime entrypoints + at compile time and forces use of `threadschedule::abi::*`. + +Use `THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(...)` to keep exported +signatures honest: + +```cpp +#include + +THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT( + void, + ::threadschedule::abi::registry_handle, + ::threadschedule::abi::thread_info_callback, + void*); + +// This would fail the static_assert: +// THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(void, ::threadschedule::ThreadRegistry*); +``` + +## Typical plugin pattern + +```cpp +#include +#include + +extern "C" void plugin_set_registry(threadschedule::abi::registry_handle handle) +{ + threadschedule::abi::set_external_registry(handle); +} + +extern "C" void plugin_start_worker() +{ + std::thread([] { + threadschedule::abi::AutoRegisterCurrentThread guard("plugin-worker", "plugin"); + // ... work ... + }).detach(); +} +``` + +This keeps the boundary on opaque handles, views, and callbacks while the +implementation still uses the full C++ API internally. + +## Byte-level mental model + +If you want to reason about the stable ABI at the "what exact bytes cross the +DSO boundary?" level, think in terms of tiny POD-like packets, not C++ objects +with hidden allocator or vtable state. + +Important scope note: + +- the stable ABI is for separately built binaries that target the same platform + ABI, for example 64-bit little-endian Linux +- it is not a promise that a 32-bit build and a 64-bit build have identical + layouts +- the examples below assume a typical 64-bit build with 8-byte pointers, + 8-byte `std::size_t`, and 4-byte `Tid` + +At that level, the core stable ABI types look like this: + +```cpp +struct registry_handle { + void* opaque; // 8 bytes on a typical 64-bit build +}; + +enum class status_code : std::uint32_t { + ok = 0, + invalid_argument = 1, + runtime_error = 2, +}; + +struct status { + status_code code; // 4 bytes +}; + +struct string_ref { + char const* data; // pointer to bytes owned elsewhere + std::size_t size; // byte count, not including any trailing NUL +}; + +struct thread_info_view { + Tid tid; // usually 4 bytes + string_ref name; // pointer + size + string_ref component_tag; // pointer + size + std::uint8_t alive; // 0 or 1 + std::uint8_t has_control_block; // 0 or 1 + std::uint8_t reserved[6]; // currently zeroed +}; +``` + +What does that mean in memory? + +`registry_handle` is just an opaque pointer-sized value. On a 64-bit build, a +handle might look like: + +```text +offset bytes +0x00 80 4a 37 91 fc 7f 00 00 -> opaque = 0x00007ffc91374a80 +``` + +The consumer must not interpret that address as a `ThreadRegistry` object. It +only stores the pointer and passes it back into other `threadschedule::abi::*` +entrypoints. + +`status` is even smaller. Because `status_code` has an explicit +`std::uint32_t` underlying type, the object is just one 32-bit integer: + +```text +status{status_code::ok} -> 00 00 00 00 +status{status_code::invalid_argument} -> 01 00 00 00 +status{status_code::runtime_error} -> 02 00 00 00 +``` + +On little-endian machines, the least significant byte comes first, which is +why `invalid_argument` appears as `01 00 00 00`. + +The more interesting case is `thread_info_view`. Suppose the runtime is about +to invoke the callback for this thread: + +- `tid = 0x0000162e` (decimal `5678`) +- `name = "io-7"` (4 bytes) +- `component_tag = "plugin"` (6 bytes) +- `alive = true` +- `has_control_block = true` + +The runtime bridge constructs it like this: + +```cpp +::threadschedule::abi::thread_info_view view{ + info.tid, + {info.name.data(), info.name.size()}, + {info.componentTag.data(), info.componentTag.size()}, + static_cast(info.alive ? 1U : 0U), + static_cast(info.control ? 1U : 0U), + {0, 0, 0, 0, 0, 0}, +}; +``` + +On a typical 64-bit little-endian target, the object layout is: + +```text +thread_info_view at 0x00007ffc913749f0 + +offset size field +0x00 4 tid +0x04 4 alignment padding +0x08 8 name.data +0x10 8 name.size +0x18 8 component_tag.data +0x20 8 component_tag.size +0x28 1 alive +0x29 1 has_control_block +0x2a 6 reserved +``` + +One concrete byte dump could look like: + +```text +offset bytes meaning +0x00 2e 16 00 00 tid = 0x0000162e +0x04 ?? ?? ?? ?? alignment padding +0x08 40 30 10 52 fc 7f 00 00 name.data = 0x00007ffc52103040 +0x10 04 00 00 00 00 00 00 00 name.size = 4 +0x18 48 30 10 52 fc 7f 00 00 component_tag.data = 0x00007ffc52103048 +0x20 06 00 00 00 00 00 00 00 component_tag.size = 6 +0x28 01 alive = true +0x29 01 has_control_block = true +0x2a 00 00 00 00 00 00 reserved bytes +``` + +The pointed-to string bytes live somewhere else: + +```text +0x00007ffc52103040: 69 6f 2d 37 "io-7" +0x00007ffc52103048: 70 6c 75 67 69 6e "plugin" +``` + +Two details matter here: + +- `string_ref` is a borrowed view, not an owning string. The ABI object + contains a pointer and a byte count, nothing more. +- the bytes are not required to be NUL-terminated. `"io-7"` crosses the + boundary as exactly 4 bytes, not as a `std::string` object. + +At the bit level, the boolean-like flags are intentionally plain one-byte +values: + +```text +alive = 1 -> 00000001 +has_control_block = 1 -> 00000001 +alive = 0 -> 00000000 +``` + +So the callback boundary sees small fixed-width integers instead of compiler- +dependent C++ `bool` layout choices embedded inside a larger exported class. + +One more subtle but important point: the `thread_info_view` object itself is a +temporary stack object inside the runtime bridge, and its `name.data` / +`component_tag.data` pointers refer to bytes owned by the underlying registry +entry. That means: + +- the callback may read the view during the callback invocation +- if the callback wants to keep the strings or the whole record, it must copy + them +- consumers must not cache the raw `thread_info_view const*` pointer after the + callback returns + +That is the core design difference from exporting full C++ registry types: + +- the stable ABI exports tiny records with explicit fields, sizes, and + ownership rules +- no `std::string`, no template-instantiated error object, no hidden allocator + state, no vtable, no exception metadata crosses the boundary +- the complex C++ objects stay inside one binary, and only plain data views + cross the seam + +Once you look at it this way, the value of the stable ABI subset becomes very +concrete: both sides agree on the exact packet shape in memory, even if one +side was built as C++17 and the other as C++23. + +## Why this matters visually + +For C++ newcomers: ABI means the binary-level contract between separately built +parts of a program. If one DSO thinks a type looks one way and another DSO +thinks it looks slightly differently, the code may still compile but can break +at runtime. + +### Without the stable ABI subset + +Here the library boundary exports ThreadSchedule C++ types directly. That is +fragile when the middle library and the final executable are built with +different standards, standard library versions, or compiler settings. + +```mermaid +flowchart LR + App["App / Consumer
C++23"] + Mid["Intermediate DSO
C++17"] + Runtime["ThreadSchedule Runtime"] + ABI["Exported C++ ABI:
ThreadRegistry / expected / STL-rich types"] + Drift["Different binary layout or calling assumptions"] + Result["Compiles, but runtime misbehavior is possible
wrong object layout, invalid calls, subtle crashes"] + + App --> ABI + Mid --> ABI + Mid --> Runtime + App --> Runtime + ABI --> Drift + Drift --> Result + + classDef bad fill:#8b1e1e,color:#ffffff,stroke:#5c1010 + class ABI,Drift,Result bad +``` + +Typical failure mode: + +- the intermediate DSO exports a type like `threadschedule::expected` or + `ThreadRegistry*` in its own ABI +- another component was built under a different language mode and interprets + that exported type with different binary assumptions +- the handoff crosses the shared-library boundary and undefined behavior starts + +### With `threadschedule::abi::*` + +Here the boundary stays on simple handles, views, callbacks, and status codes. +Those are intentionally small and stable so each side agrees on the binary +shape even if they were built differently. + +```mermaid +flowchart LR + App2["App / Consumer
C++23"] + Mid2["Intermediate DSO
C++17"] + Runtime2["ThreadSchedule Runtime"] + Stable["Stable ABI boundary:
registry_handle / thread_info_view / status"] + Internal["Each side may still use the full C++ API internally"] + Safe["Shared-library boundary stays predictable and portable"] + + App2 --> Stable + Mid2 --> Stable + Stable --> Runtime2 + Mid2 --> Internal + App2 --> Internal + Stable --> Safe + + classDef good fill:#1f6f3d,color:#ffffff,stroke:#124628 + class Stable,Safe good +``` + +Mental model: + +- use the full ThreadSchedule C++ API inside one binary that you build + together +- use `threadschedule::abi::*` only at the seam between separately built + binaries +- once the boundary is reduced to opaque handles and plain views, the runtime + can do the complex C++ work behind that seam safely + +## Concrete mixed-standard Conan scenario + +This is the kind of failure that motivated the stable ABI work: + +- there is only one shared `ThreadSchedule::Runtime` in the process +- `libA` is consumed as a Conan package and was built earlier as C++17 +- your application is built as C++23 +- `libA` exposes richer ThreadSchedule-adjacent C++ types in public headers, + for example a struct that contains `ThreadWrapper` or APIs that surface + ThreadSchedule result types directly +- inside `libA`, `ThreadWrapper::set_name(...)` is called + +On Linux, thread names longer than 15 characters fail with +`invalid_argument`. That means the bug may stay hidden for a long time and only +become visible when the code takes the error path. + +```mermaid +sequenceDiagram + participant App as App
built as C++23 + participant LibA as Conan package / DSO
built as C++17 + participant RT as Shared ThreadSchedule::Runtime + participant TS as ThreadSchedule API in libA + participant OS as OS thread naming + + App->>LibA: call exported API from public header + Note over App,LibA: Header boundary exposes rich C++ ThreadSchedule types + LibA->>TS: wrapper.set_name("name-longer-than-15") + TS->>OS: pthread_setname_np(...) + OS-->>TS: invalid_argument + TS-->>LibA: expected in error state + TS->>RT: same shared runtime as the app uses + LibA-->>App: error-bearing C++ object crosses DSO boundary + Note over App: Problem is not two runtimes.
Problem is the C++ ABI seam between LibA and App. + Note over App: Consumer interprets returned bytes using its own build assumptions +``` + +Why this was so confusing in practice: + +- short names often stayed on the success path, so nothing obviously broke +- the failure only appeared once the long-name validation forced an error value +- because the boundary was a rich C++ ABI, the visible symptom appeared far + away from the real design mistake + +## Byte-level view of the `expected`-style ABI bug + +What actually goes wrong at machine level is not "the runtime returned the +wrong value". The problem is: producer and consumer emit code that reads +different offsets from the same returned bytes. + +Important precision: + +- the exact object layout of `std::expected` is not + standardized and depends on compiler + standard library implementation +- the example below is illustrative, not a claim about one exact libstdc++ or + libc++ layout +- the failure mode is real because both sides compile field access into fixed + byte offsets, and those offsets only work if both sides agree on the layout + +Imagine a rich C++ result object crossing the DSO boundary in error state. +Internally it needs to represent at least two things: + +- a discriminator: "success or error?" +- an error payload: here some `std::error_code`-like object + +One side might effectively lay it out like this: + +```text +LibA / producer view of ResultA + +offset size field +0x00 1 has_value +0x01 3 padding +0x04 4 error.value +0x08 8 error.category_ptr +``` + +So when `set_name("name-longer-than-15")` fails with `invalid_argument`, LibA +could physically return bytes like: + +```text +address bytes producer meaning +0x00 00 has_value = false +0x01 00 00 00 padding +0x04 16 00 00 00 error.value = 22 (EINVAL) +0x08 30 b4 55 91 7a 7f 00 00 error.category_ptr +``` + +Now imagine the consumer was built with a different implementation or mode and +compiled code under a different assumption: + +```text +App / consumer view of ResultB + +offset size field +0x00 8 error.category_ptr +0x08 4 error.value +0x0c 1 has_value +0x0d 3 padding +``` + +The app code then emits loads that are valid for `ResultB`, for example: + +```text +load 8 bytes from +0x00 -> expects error.category_ptr +load 4 bytes from +0x08 -> expects error.value +load 1 byte from +0x0c -> expects has_value +``` + +But the actual bytes in memory came from `ResultA`, not `ResultB`. So the app +interprets them like this: + +```text +bytes at +0x00: 00 00 00 00 16 00 00 00 +consumer reads that as error.category_ptr = 0x0000001600000000 // bogus + +bytes at +0x08: 30 b4 55 91 +consumer reads that as error.value = 0x9155b430 // nonsense + +byte at +0x0c: 7a +consumer reads that as has_value = true // wrong +``` + +That is the real ABI break: + +- LibA wrote correct bytes for the type it compiled +- App read correct offsets for the type it compiled +- the returned byte sequence itself was reinterpreted under the wrong schema + +From the CPU's point of view there is no "`expected` object" anymore. There is +only: + +- "read 8 bytes at offset 0" +- "read 4 bytes at offset 8" +- "branch depending on byte at offset 12" + +If producer and consumer disagree about what lives at those offsets, undefined +behavior starts immediately. + +This is why such bugs often look random: + +- one build may mostly observe the success path, where only the discriminator + gets checked +- another build may hit the error path and start dereferencing what it thinks + is an `error_category*` +- the crash can happen far away from the original failing call because the + wrongly decoded object may first get copied, logged, or inspected later + +The current `threadschedule::expected` hardening exists specifically to avoid +that kind of standard-library-dependent object drift in public APIs. But the +general lesson remains broader: rich C++ result objects are poor DSO boundary +types because the consumer must know their exact byte schema to read them +correctly. + +## Important nuance + +- the recent `threadschedule::expected` hardening removes one concrete source + of standard-mode drift +- the stable ABI subset is still needed because types like `ThreadWrapper`, + `ThreadRegistry`, and STL-heavy exported structs remain poor DSO boundary + types even after `expected` is fixed diff --git a/include/threadschedule/abi.hpp b/include/threadschedule/abi.hpp new file mode 100644 index 0000000..ad0a30d --- /dev/null +++ b/include/threadschedule/abi.hpp @@ -0,0 +1,301 @@ +#pragma once + +/** + * @file abi.hpp + * @brief Stable ABI helpers and runtime-safe registry accessors. + */ + +#include "export.hpp" +#include "scheduler_policy.hpp" +#include +#include +#include +#include +#include + +namespace threadschedule::abi +{ + +struct string_ref +{ + char const* data = nullptr; + std::size_t size = 0; + + [[nodiscard]] constexpr auto view() const noexcept -> std::string_view + { + return std::string_view(data != nullptr ? data : "", size); + } +}; + +struct registry_handle +{ + void* opaque = nullptr; + + [[nodiscard]] constexpr auto valid() const noexcept -> bool + { + return opaque != nullptr; + } +}; + +enum class status_code : std::uint32_t +{ + ok = 0, + invalid_argument = 1, + runtime_error = 2, +}; + +struct status +{ + status_code code = status_code::ok; +}; + +struct thread_info_view +{ + Tid tid{}; + string_ref name{}; + string_ref component_tag{}; + std::uint8_t alive = 0; + std::uint8_t has_control_block = 0; + std::uint8_t reserved[6]{}; +}; + +using thread_info_callback = void (*)(thread_info_view const*, void*); + +template +struct is_abi_stable : std::false_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template +struct is_abi_stable : std::bool_constant || std::is_same_v, char>> +{ +}; + +template +struct is_abi_stable : is_abi_stable +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template <> +struct is_abi_stable : std::true_type +{ +}; + +template +inline constexpr bool is_abi_stable_v = is_abi_stable>>::value; + +template +struct is_stable_signature : std::false_type +{ +}; + +template +struct is_stable_signature + : std::bool_constant && (... && is_abi_stable_v)> +{ +}; + +template +inline constexpr bool is_stable_signature_v = is_stable_signature::value; + +#if __cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) +template +concept StableAbiType = is_abi_stable_v; +#endif + +[[nodiscard]] constexpr auto succeeded(status value) noexcept -> bool +{ + return value.code == status_code::ok; +} + +[[nodiscard]] constexpr auto make_string_ref(std::string_view value) noexcept -> string_ref +{ + return string_ref{value.data(), value.size()}; +} + +} // namespace threadschedule::abi + +#define THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(ReturnType, ...) \ + static_assert(::threadschedule::abi::is_stable_signature_v, \ + "Signature is not part of the ThreadSchedule stable ABI subset") + +#if defined(THREADSCHEDULE_RUNTIME) +extern "C" +{ +THREADSCHEDULE_API auto threadschedule_abi_registry_create() noexcept -> ::threadschedule::abi::registry_handle; +THREADSCHEDULE_API void threadschedule_abi_registry_destroy(::threadschedule::abi::registry_handle handle) noexcept; +THREADSCHEDULE_API auto threadschedule_abi_registry_current() noexcept -> ::threadschedule::abi::registry_handle; +THREADSCHEDULE_API void threadschedule_abi_registry_set_external(::threadschedule::abi::registry_handle handle) noexcept; +THREADSCHEDULE_API auto threadschedule_abi_registry_count(::threadschedule::abi::registry_handle handle) noexcept + -> std::size_t; +THREADSCHEDULE_API auto threadschedule_abi_registry_for_each(::threadschedule::abi::registry_handle handle, + ::threadschedule::abi::thread_info_callback callback, + void* user_data) noexcept -> ::threadschedule::abi::status; +THREADSCHEDULE_API auto threadschedule_abi_registry_register_current_thread(::threadschedule::abi::registry_handle handle, + char const* name, std::size_t name_size, + char const* component_tag, + std::size_t component_tag_size) noexcept + -> ::threadschedule::abi::status; +THREADSCHEDULE_API auto threadschedule_abi_registry_unregister_current_thread( + ::threadschedule::abi::registry_handle handle) noexcept -> ::threadschedule::abi::status; +} + +namespace threadschedule::abi +{ + +[[nodiscard]] inline auto create_registry() noexcept -> registry_handle +{ + return threadschedule_abi_registry_create(); +} + +inline void destroy_registry(registry_handle handle) noexcept +{ + threadschedule_abi_registry_destroy(handle); +} + +[[nodiscard]] inline auto current_registry() noexcept -> registry_handle +{ + return threadschedule_abi_registry_current(); +} + +inline void set_external_registry(registry_handle handle) noexcept +{ + threadschedule_abi_registry_set_external(handle); +} + +[[nodiscard]] inline auto registry_count(registry_handle handle) noexcept -> std::size_t +{ + return threadschedule_abi_registry_count(handle); +} + +inline auto registry_for_each(registry_handle handle, thread_info_callback callback, void* user_data = nullptr) noexcept + -> status +{ + return threadschedule_abi_registry_for_each(handle, callback, user_data); +} + +inline auto register_current_thread(registry_handle handle, std::string_view name = {}, + std::string_view component_tag = {}) noexcept -> status +{ + return threadschedule_abi_registry_register_current_thread(handle, name.data(), name.size(), component_tag.data(), + component_tag.size()); +} + +inline auto unregister_current_thread(registry_handle handle) noexcept -> status +{ + return threadschedule_abi_registry_unregister_current_thread(handle); +} + +class AutoRegisterCurrentThread +{ + public: + explicit AutoRegisterCurrentThread(std::string_view name = {}, std::string_view component_tag = {}) noexcept + : handle_(current_registry()), + active_(succeeded(register_current_thread(handle_, name, component_tag))) + { + } + + explicit AutoRegisterCurrentThread(registry_handle handle, std::string_view name = {}, + std::string_view component_tag = {}) noexcept + : handle_(handle), active_(succeeded(register_current_thread(handle_, name, component_tag))) + { + } + + ~AutoRegisterCurrentThread() + { + if (active_) + (void)unregister_current_thread(handle_); + } + + AutoRegisterCurrentThread(AutoRegisterCurrentThread const&) = delete; + auto operator=(AutoRegisterCurrentThread const&) -> AutoRegisterCurrentThread& = delete; + + AutoRegisterCurrentThread(AutoRegisterCurrentThread&& other) noexcept + : handle_(other.handle_), active_(std::exchange(other.active_, false)) + { + } + + auto operator=(AutoRegisterCurrentThread&& other) noexcept -> AutoRegisterCurrentThread& + { + if (this != &other) + { + if (active_) + (void)unregister_current_thread(handle_); + handle_ = other.handle_; + active_ = std::exchange(other.active_, false); + } + return *this; + } + + [[nodiscard]] auto active() const noexcept -> bool + { + return active_; + } + + private: + registry_handle handle_{}; + bool active_ = false; +}; + +} // namespace threadschedule::abi +#endif diff --git a/include/threadschedule/chaos.hpp b/include/threadschedule/chaos.hpp index cf64300..eb7b45f 100644 --- a/include/threadschedule/chaos.hpp +++ b/include/threadschedule/chaos.hpp @@ -134,8 +134,8 @@ class ChaosController std::mt19937 rng(std::random_device{}()); while (!stop_) { - registry().apply(pred, [&](RegisteredThreadInfo const& info) { - auto blk = registry().get(info.tid); + detail::runtime_registry().apply(pred, [&](RegisteredThreadInfo const& info) { + auto blk = detail::runtime_registry().get(info.tid); (void)blk; }); @@ -144,10 +144,10 @@ class ChaosController { auto topo = read_topology(); size_t idx = 0; - registry().apply(pred, [&](RegisteredThreadInfo const& info) { + detail::runtime_registry().apply(pred, [&](RegisteredThreadInfo const& info) { ThreadAffinity aff = affinity_for_node( static_cast(idx % (topo.numa_nodes > 0 ? topo.numa_nodes : 1)), static_cast(idx)); - (void)registry().set_affinity(info.tid, aff); + (void)detail::runtime_registry().set_affinity(info.tid, aff); ++idx; }); } @@ -156,7 +156,7 @@ class ChaosController if (config_.priority_jitter != 0) { std::uniform_int_distribution dist(-config_.priority_jitter, config_.priority_jitter); - registry().apply(pred, [&](RegisteredThreadInfo const& info) { + detail::runtime_registry().apply(pred, [&](RegisteredThreadInfo const& info) { int delta = dist(rng); int baseline = ThreadPriority::normal().value(); #ifndef _WIN32 @@ -164,7 +164,7 @@ class ChaosController if (sched_getparam(info.tid, &sp) == 0) baseline = sp.sched_priority; #endif - (void)registry().set_priority(info.tid, ThreadPriority{baseline + delta}); + (void)detail::runtime_registry().set_priority(info.tid, ThreadPriority{baseline + delta}); }); } diff --git a/include/threadschedule/export.hpp b/include/threadschedule/export.hpp new file mode 100644 index 0000000..7258c93 --- /dev/null +++ b/include/threadschedule/export.hpp @@ -0,0 +1,39 @@ +#pragma once + +/** + * @file export.hpp + * @brief Shared export and ABI feature macros for ThreadSchedule. + */ + +#if defined(_WIN32) || defined(_WIN64) +# if defined(THREADSCHEDULE_EXPORTS) +# define THREADSCHEDULE_API __declspec(dllexport) +# else +# define THREADSCHEDULE_API __declspec(dllimport) +# endif +#elif defined(__GNUC__) || defined(__clang__) +# define THREADSCHEDULE_API __attribute__((visibility("default"))) +#else +# define THREADSCHEDULE_API +#endif + +#if defined(__has_cpp_attribute) +# if __has_cpp_attribute(deprecated) +# define THREADSCHEDULE_DEPRECATED(msg) [[deprecated(msg)]] +# else +# define THREADSCHEDULE_DEPRECATED(msg) +# endif +#else +# define THREADSCHEDULE_DEPRECATED(msg) +#endif + +#if defined(THREADSCHEDULE_STABLE_ABI_STRICT) && !defined(THREADSCHEDULE_STABLE_ABI) +# define THREADSCHEDULE_STABLE_ABI 1 +#endif + +#if defined(THREADSCHEDULE_RUNTIME) && defined(THREADSCHEDULE_STABLE_ABI) && \ + !defined(THREADSCHEDULE_STABLE_ABI_STRICT) && !defined(THREADSCHEDULE_INTERNAL_RUNTIME_BUILD) +# define THREADSCHEDULE_RUNTIME_ABI_UNSAFE_DEPRECATED(msg) THREADSCHEDULE_DEPRECATED(msg) +#else +# define THREADSCHEDULE_RUNTIME_ABI_UNSAFE_DEPRECATED(msg) +#endif diff --git a/include/threadschedule/thread_registry.hpp b/include/threadschedule/thread_registry.hpp index 5ee24fb..1676487 100644 --- a/include/threadschedule/thread_registry.hpp +++ b/include/threadschedule/thread_registry.hpp @@ -6,10 +6,12 @@ */ #include "callable.hpp" +#include "export.hpp" #include "expected.hpp" #if defined(THREADSCHEDULE_HAS_REFLECTION) && THREADSCHEDULE_HAS_REFLECTION #include "reflection.hpp" #endif +#include "abi.hpp" #include "scheduler_policy.hpp" #include "thread_wrapper.hpp" // for ThreadInfo, ThreadAffinity #include @@ -35,17 +37,6 @@ namespace threadschedule { -// Optional export macro for building a runtime (shared/dll) variant -#if defined(_WIN32) || defined(_WIN64) - #if defined(THREADSCHEDULE_EXPORTS) - #define THREADSCHEDULE_API __declspec(dllexport) - #else - #define THREADSCHEDULE_API __declspec(dllimport) - #endif -#else - #define THREADSCHEDULE_API __attribute__((visibility("default"))) -#endif - /** * @brief Snapshot of metadata for a single registered thread. * @@ -888,9 +879,11 @@ class ThreadRegistry : public detail::QueryFacadeMixin * @{ */ +namespace detail +{ #if defined(THREADSCHEDULE_RUNTIME) -THREADSCHEDULE_API auto registry() -> ThreadRegistry&; -THREADSCHEDULE_API void set_external_registry(ThreadRegistry* reg); +THREADSCHEDULE_API auto runtime_registry() -> ThreadRegistry&; +THREADSCHEDULE_API void runtime_set_external_registry(ThreadRegistry* reg); #else /** @cond INTERNAL */ inline auto registry_storage() -> ThreadRegistry*& @@ -900,6 +893,52 @@ inline auto registry_storage() -> ThreadRegistry*& } /** @endcond */ +inline auto runtime_registry() -> ThreadRegistry& +{ + ThreadRegistry*& ext = registry_storage(); + if (ext != nullptr) + return *ext; + static ThreadRegistry local; + return local; +} + +inline void runtime_set_external_registry(ThreadRegistry* reg) +{ + registry_storage() = reg; +} +#endif +} // namespace detail + +#if defined(THREADSCHEDULE_RUNTIME) && defined(THREADSCHEDULE_STABLE_ABI_STRICT) && \ + !defined(THREADSCHEDULE_INTERNAL_RUNTIME_BUILD) +template +auto registry() -> ThreadRegistry& +{ + static_assert(!std::is_same_v, + "threadschedule::registry() is not part of the stable ABI subset. " + "Use threadschedule::abi::current_registry() and related helpers instead."); + return detail::runtime_registry(); +} + +template +void set_external_registry(ThreadRegistry*) +{ + static_assert(!std::is_same_v, + "threadschedule::set_external_registry(ThreadRegistry*) is not part of the stable ABI subset. " + "Use threadschedule::abi::set_external_registry(threadschedule::abi::registry_handle) instead."); +} +#elif defined(THREADSCHEDULE_RUNTIME) +THREADSCHEDULE_RUNTIME_ABI_UNSAFE_DEPRECATED( + "threadschedule::registry() is not part of the stable ABI subset; use threadschedule::abi::current_registry() " + "and related helpers for DSO boundaries") +THREADSCHEDULE_API auto registry() -> ThreadRegistry&; + +THREADSCHEDULE_RUNTIME_ABI_UNSAFE_DEPRECATED( + "threadschedule::set_external_registry(ThreadRegistry*) is not part of the stable ABI subset; use " + "threadschedule::abi::set_external_registry(registry_handle) for DSO boundaries") +THREADSCHEDULE_API void set_external_registry(ThreadRegistry* reg); +#else + /** * @brief Returns a reference to the process-wide @ref ThreadRegistry. * @@ -911,11 +950,7 @@ inline auto registry_storage() -> ThreadRegistry*& */ inline auto registry() -> ThreadRegistry& { - ThreadRegistry*& ext = registry_storage(); - if (ext != nullptr) - return *ext; - static ThreadRegistry local; - return local; + return detail::runtime_registry(); } /** @@ -936,7 +971,7 @@ inline auto registry() -> ThreadRegistry& */ inline void set_external_registry(ThreadRegistry* reg) { - registry_storage() = reg; + detail::runtime_set_external_registry(reg); } /** @} */ #endif @@ -956,6 +991,19 @@ enum class BuildMode : std::uint8_t RUNTIME ///< Core symbols are compiled into a shared library. }; +} // namespace threadschedule + +namespace threadschedule::abi +{ +template <> +struct is_abi_stable<::threadschedule::BuildMode> : std::true_type +{ +}; +} // namespace threadschedule::abi + +namespace threadschedule +{ + #if defined(THREADSCHEDULE_RUNTIME) inline constexpr bool is_runtime_build = true; ///< @c true when compiled with @c THREADSCHEDULE_RUNTIME. @@ -1093,15 +1141,33 @@ class CompositeThreadRegistry : public detail::QueryFacadeMixin AutoRegisterCurrentThread& = delete; + AutoRegisterCurrentThread(AutoRegisterCurrentThread&&) noexcept = delete; + auto operator=(AutoRegisterCurrentThread&&) noexcept -> AutoRegisterCurrentThread& = delete; +#else + THREADSCHEDULE_RUNTIME_ABI_UNSAFE_DEPRECATED( + "threadschedule::AutoRegisterCurrentThread is not part of the stable ABI subset; use " + "threadschedule::abi::AutoRegisterCurrentThread for DSO boundaries") explicit AutoRegisterCurrentThread(std::string const& name = std::string(), std::string const& componentTag = std::string()) : active_(true), externalReg_(nullptr) { auto block = ThreadControlBlock::create_for_current_thread(); (void)block->set_name(name); - registry().register_current_thread(block, name, componentTag); + detail::runtime_registry().register_current_thread(block, name, componentTag); } + THREADSCHEDULE_RUNTIME_ABI_UNSAFE_DEPRECATED( + "threadschedule::AutoRegisterCurrentThread(ThreadRegistry&) is not part of the stable ABI subset; use " + "threadschedule::abi::AutoRegisterCurrentThread for DSO boundaries") explicit AutoRegisterCurrentThread(ThreadRegistry& reg, std::string const& name = std::string(), std::string const& componentTag = std::string()) : active_(true), externalReg_(®) @@ -1117,7 +1183,7 @@ class AutoRegisterCurrentThread if (externalReg_ != nullptr) externalReg_->unregister_current_thread(); else - registry().unregister_current_thread(); + detail::runtime_registry().unregister_current_thread(); } } AutoRegisterCurrentThread(AutoRegisterCurrentThread const&) = delete; @@ -1137,7 +1203,7 @@ class AutoRegisterCurrentThread if (externalReg_ != nullptr) externalReg_->unregister_current_thread(); else - registry().unregister_current_thread(); + detail::runtime_registry().unregister_current_thread(); } active_ = other.active_; externalReg_ = other.externalReg_; @@ -1146,10 +1212,11 @@ class AutoRegisterCurrentThread } return *this; } +#endif private: - bool active_; - ThreadRegistry* externalReg_; + bool active_{false}; + ThreadRegistry* externalReg_{nullptr}; }; } // namespace threadschedule diff --git a/integration_tests/runtime_abi_compat/libA/CMakeLists.txt b/integration_tests/runtime_abi_compat/libA/CMakeLists.txt index 28f3c06..35abc21 100644 --- a/integration_tests/runtime_abi_compat/libA/CMakeLists.txt +++ b/integration_tests/runtime_abi_compat/libA/CMakeLists.txt @@ -19,7 +19,7 @@ target_link_libraries(runtime_abi_libA ThreadSchedule::Runtime ) -target_compile_features(runtime_abi_libA PUBLIC cxx_std_17) +target_compile_features(runtime_abi_libA PUBLIC cxx_std_23) if(WIN32) target_compile_definitions(runtime_abi_libA PRIVATE BUILD_RUNTIME_LIBA_SHARED) @@ -47,4 +47,3 @@ install(EXPORT RuntimeAbiLibATargets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/RuntimeAbiLibA ) - diff --git a/integration_tests/runtime_abi_compat/main_app/CMakeLists.txt b/integration_tests/runtime_abi_compat/main_app/CMakeLists.txt index 30a1c8d..f3c63c4 100644 --- a/integration_tests/runtime_abi_compat/main_app/CMakeLists.txt +++ b/integration_tests/runtime_abi_compat/main_app/CMakeLists.txt @@ -10,7 +10,7 @@ target_link_libraries(runtime_abi_main PRIVATE runtime_abi_libA runtime_abi_libB ) -target_compile_features(runtime_abi_main PRIVATE cxx_std_17) +target_compile_features(runtime_abi_main PRIVATE cxx_std_23) add_custom_command(TARGET runtime_abi_main POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different @@ -37,4 +37,3 @@ if(UNIX AND NOT APPLE) ENVIRONMENT "LD_LIBRARY_PATH=$") endif() - diff --git a/src/runtime_registry.cpp b/src/runtime_registry.cpp index 0ce8400..f56c1a3 100644 --- a/src/runtime_registry.cpp +++ b/src/runtime_registry.cpp @@ -1,4 +1,6 @@ +#include #include +#include namespace threadschedule { @@ -6,19 +8,130 @@ namespace threadschedule static ThreadRegistry* g_external = nullptr; static ThreadRegistry g_local; -THREADSCHEDULE_API auto registry() -> ThreadRegistry& +static auto resolve_registry(abi::registry_handle handle) noexcept -> ThreadRegistry* +{ + return static_cast(handle.opaque); +} + +static auto active_registry() noexcept -> ThreadRegistry& { return (g_external != nullptr) ? *g_external : g_local; } -THREADSCHEDULE_API void set_external_registry(ThreadRegistry* reg) +THREADSCHEDULE_API auto detail::runtime_registry() -> ThreadRegistry& +{ + return active_registry(); +} + +THREADSCHEDULE_API void detail::runtime_set_external_registry(ThreadRegistry* reg) { g_external = reg; } +THREADSCHEDULE_API auto registry() -> ThreadRegistry& +{ + return detail::runtime_registry(); +} + +THREADSCHEDULE_API void set_external_registry(ThreadRegistry* reg) +{ + detail::runtime_set_external_registry(reg); +} + THREADSCHEDULE_API auto build_mode() -> BuildMode { return BuildMode::RUNTIME; } } // namespace threadschedule + +extern "C" +{ + +THREADSCHEDULE_API auto threadschedule_abi_registry_create() noexcept -> ::threadschedule::abi::registry_handle +{ + return ::threadschedule::abi::registry_handle{new ::threadschedule::ThreadRegistry()}; +} + +THREADSCHEDULE_API void threadschedule_abi_registry_destroy(::threadschedule::abi::registry_handle handle) noexcept +{ + auto* reg = static_cast<::threadschedule::ThreadRegistry*>(handle.opaque); + if (reg == nullptr || reg == &::threadschedule::g_local) + return; + if (::threadschedule::g_external == reg) + ::threadschedule::g_external = nullptr; + delete reg; +} + +THREADSCHEDULE_API auto threadschedule_abi_registry_current() noexcept -> ::threadschedule::abi::registry_handle +{ + return ::threadschedule::abi::registry_handle{&::threadschedule::active_registry()}; +} + +THREADSCHEDULE_API void threadschedule_abi_registry_set_external(::threadschedule::abi::registry_handle handle) noexcept +{ + ::threadschedule::g_external = static_cast<::threadschedule::ThreadRegistry*>(handle.opaque); +} + +THREADSCHEDULE_API auto threadschedule_abi_registry_count(::threadschedule::abi::registry_handle handle) noexcept + -> std::size_t +{ + auto* reg = ::threadschedule::resolve_registry(handle); + return reg != nullptr ? reg->count() : 0U; +} + +THREADSCHEDULE_API auto threadschedule_abi_registry_for_each(::threadschedule::abi::registry_handle handle, + ::threadschedule::abi::thread_info_callback callback, + void* user_data) noexcept -> ::threadschedule::abi::status +{ + auto* reg = ::threadschedule::resolve_registry(handle); + if (reg == nullptr || callback == nullptr) + return {::threadschedule::abi::status_code::invalid_argument}; + + reg->for_each([&](::threadschedule::RegisteredThreadInfo const& info) { + ::threadschedule::abi::thread_info_view view{ + info.tid, + {info.name.data(), info.name.size()}, + {info.componentTag.data(), info.componentTag.size()}, + static_cast(info.alive ? 1U : 0U), + static_cast(info.control ? 1U : 0U), + {0, 0, 0, 0, 0, 0}, + }; + callback(&view, user_data); + }); + + return {::threadschedule::abi::status_code::ok}; +} + +THREADSCHEDULE_API auto threadschedule_abi_registry_register_current_thread(::threadschedule::abi::registry_handle handle, + char const* name, std::size_t name_size, + char const* component_tag, + std::size_t component_tag_size) noexcept + -> ::threadschedule::abi::status +{ + auto* reg = ::threadschedule::resolve_registry(handle); + if (reg == nullptr) + return {::threadschedule::abi::status_code::invalid_argument}; + + std::string const name_value = (name != nullptr) ? std::string(name, name_size) : std::string(); + std::string const component_value = + (component_tag != nullptr) ? std::string(component_tag, component_tag_size) : std::string(); + + auto block = ::threadschedule::ThreadControlBlock::create_for_current_thread(); + (void)block->set_name(name_value); + reg->register_current_thread(block, name_value, component_value); + return {::threadschedule::abi::status_code::ok}; +} + +THREADSCHEDULE_API auto threadschedule_abi_registry_unregister_current_thread( + ::threadschedule::abi::registry_handle handle) noexcept -> ::threadschedule::abi::status +{ + auto* reg = ::threadschedule::resolve_registry(handle); + if (reg == nullptr) + return {::threadschedule::abi::status_code::invalid_argument}; + + reg->unregister_current_thread(); + return {::threadschedule::abi::status_code::ok}; +} + +} // extern "C" diff --git a/src/threadschedule.cppm b/src/threadschedule.cppm index 03833f2..f17a939 100644 --- a/src/threadschedule.cppm +++ b/src/threadschedule.cppm @@ -74,7 +74,7 @@ using ::threadschedule::AutoRegisterCurrentThread; using ::threadschedule::registry; using ::threadschedule::set_external_registry; #ifndef THREADSCHEDULE_RUNTIME -using ::threadschedule::registry_storage; +using ::threadschedule::detail::registry_storage; #endif // -- error_handler.hpp ------------------------------------------------------ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b8f4851..1edb8d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -78,6 +78,28 @@ if(TARGET gtest) DISCOVERY_TIMEOUT 60 PROPERTIES TIMEOUT 120 ) + + add_executable(abi_test abi_test.cpp) + target_link_libraries(abi_test + ThreadSchedule::ThreadSchedule + gtest + gtest_main + ) + if(THREADSCHEDULE_RUNTIME) + target_link_libraries(abi_test ThreadSchedule::Runtime) + if(WIN32) + add_custom_command(TARGET abi_test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + ) + endif() + endif() + gtest_discover_tests(abi_test + DISCOVERY_TIMEOUT 60 + PROPERTIES TIMEOUT 120 + ) + add_executable(thread_registry_stress_test thread_registry_stress_test.cpp) target_link_libraries(thread_registry_stress_test ThreadSchedule::ThreadSchedule @@ -176,7 +198,65 @@ if(TARGET gtest) endif() # Coroutine tests (C++20) + include(CheckCXXCompilerFlag) include(CheckCXXSourceCompiles) + set(_THREADSCHEDULE_SAVED_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}") + set(_THREADSCHEDULE_SAVED_REQUIRED_INCLUDES "${CMAKE_REQUIRED_INCLUDES}") + set(CMAKE_REQUIRED_INCLUDES "${PROJECT_SOURCE_DIR}/include") + check_cxx_compiler_flag("-Werror=deprecated-declarations" THREADSCHEDULE_HAS_WERROR_DEPRECATED) + set(_THREADSCHEDULE_DEPRECATED_AS_ERROR_FLAG "") + if(THREADSCHEDULE_HAS_WERROR_DEPRECATED) + set(_THREADSCHEDULE_DEPRECATED_AS_ERROR_FLAG " -Werror=deprecated-declarations") + endif() + set(CMAKE_REQUIRED_FLAGS + "${CMAKE_REQUIRED_FLAGS} -DTHREADSCHEDULE_RUNTIME -DTHREADSCHEDULE_STABLE_ABI=1${_THREADSCHEDULE_DEPRECATED_AS_ERROR_FLAG}" + ) + check_cxx_source_compiles(" + #include + int main() { + auto& reg = threadschedule::registry(); + return static_cast(reg.count()); + } + " THREADSCHEDULE_STABLE_ABI_DEPRECATED_RUNTIME_REGISTRY) + if(THREADSCHEDULE_HAS_WERROR_DEPRECATED AND THREADSCHEDULE_STABLE_ABI_DEPRECATED_RUNTIME_REGISTRY) + message(FATAL_ERROR "THREADSCHEDULE_STABLE_ABI must deprecate threadschedule::registry() in runtime mode") + endif() + + set(CMAKE_REQUIRED_FLAGS + "${CMAKE_REQUIRED_FLAGS} -DTHREADSCHEDULE_RUNTIME -DTHREADSCHEDULE_STABLE_ABI=1 -DTHREADSCHEDULE_STABLE_ABI_STRICT=1" + ) + check_cxx_source_compiles(" + #include + int main() { + auto& reg = threadschedule::registry(); + return static_cast(reg.count()); + } + " THREADSCHEDULE_STRICT_RUNTIME_REGISTRY_AVAILABLE) + if(THREADSCHEDULE_STRICT_RUNTIME_REGISTRY_AVAILABLE) + message(FATAL_ERROR "THREADSCHEDULE_STABLE_ABI_STRICT must reject threadschedule::registry() in runtime mode") + endif() + + check_cxx_source_compiles(" + #include + THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(void, ::threadschedule::ThreadRegistry*); + int main() { return 0; } + " THREADSCHEDULE_UNSAFE_SIGNATURE_ALLOWED) + if(THREADSCHEDULE_UNSAFE_SIGNATURE_ALLOWED) + message(FATAL_ERROR "Stable ABI signature validation must reject ThreadRegistry* exports") + endif() + + check_cxx_source_compiles(" + #include + THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(void, ::threadschedule::abi::registry_handle); + int main() { return 0; } + " THREADSCHEDULE_SAFE_SIGNATURE_ALLOWED) + if(NOT THREADSCHEDULE_SAFE_SIGNATURE_ALLOWED) + message(FATAL_ERROR "Stable ABI signature validation must accept registry_handle exports") + endif() + + set(CMAKE_REQUIRED_FLAGS "${_THREADSCHEDULE_SAVED_REQUIRED_FLAGS}") + set(CMAKE_REQUIRED_INCLUDES "${_THREADSCHEDULE_SAVED_REQUIRED_INCLUDES}") + set(CMAKE_REQUIRED_FLAGS "${CMAKE_CXX_FLAGS}") check_cxx_source_compiles(" #include diff --git a/tests/abi_test.cpp b/tests/abi_test.cpp new file mode 100644 index 0000000..3e37023 --- /dev/null +++ b/tests/abi_test.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include + +namespace ts = threadschedule; + +static_assert(ts::abi::is_abi_stable_v); +static_assert(ts::abi::is_abi_stable_v); +static_assert(!ts::abi::is_abi_stable_v); + +THREADSCHEDULE_VALIDATE_STABLE_ABI_EXPORT(void, ::threadschedule::abi::registry_handle); + +namespace +{ + +struct CallbackState +{ + std::size_t seen = 0; + bool found_name = false; + bool found_component = false; +}; + +void collect_threads(ts::abi::thread_info_view const* info, void* user_data) +{ + auto& state = *static_cast(user_data); + ++state.seen; + if (info == nullptr) + return; + + auto const name = info->name.view(); + auto const component = info->component_tag.view(); + if (name == "abi-worker") + state.found_name = true; + if (component == "stable-test") + state.found_component = true; +} + +} // namespace + +#if defined(THREADSCHEDULE_RUNTIME) +TEST(StableAbiTest, RuntimeRegistryRegistrationAndEnumeration) +{ + auto const handle = ts::abi::current_registry(); + auto const before = ts::abi::registry_count(handle); + + { + ts::abi::AutoRegisterCurrentThread guard("abi-worker", "stable-test"); + ASSERT_TRUE(guard.active()); + EXPECT_EQ(ts::abi::registry_count(handle), before + 1); + + CallbackState state{}; + auto const status = ts::abi::registry_for_each(handle, &collect_threads, &state); + EXPECT_TRUE(ts::abi::succeeded(status)); + EXPECT_GE(state.seen, before + 1); + EXPECT_TRUE(state.found_name); + EXPECT_TRUE(state.found_component); + } + + EXPECT_EQ(ts::abi::registry_count(handle), before); +} +#endif