diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d153442..0d6f168 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,39 +115,48 @@ jobs: - compiler: "clang-cl" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "clang++-cl" cc: "clang-cl" runs-on: "windows-2022" b2-toolset: "clang-win" generator-toolset: "ClangCL" is-latest: true - name: "Windows-Clang" + is-earliest: true + name: "Windows-Clang: C++20" shared: true build-type: "Release" build-cmake: true - compiler: "mingw" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "g++" cc: "gcc" runs-on: "windows-2022" b2-toolset: "gcc" generator: "MinGW Makefiles" is-latest: true - name: "MinGW (shared)" + is-earliest: true + name: "MinGW (shared): C++20" shared: true build-type: "Debug" build-cmake: true - compiler: "mingw" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "g++" cc: "gcc" runs-on: "windows-2022" b2-toolset: "gcc" generator: "MinGW Makefiles" is-latest: true - name: "MinGW (static)" + is-earliest: true + name: "MinGW (static): C++20" shared: false build-type: "Release" build-cmake: true @@ -157,58 +166,68 @@ jobs: - compiler: "apple-clang" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "clang++" cc: "clang" runs-on: "macos-26" b2-toolset: "clang" is-latest: true - name: "Apple-Clang (macOS 26)" + name: "Apple-Clang (macOS 26): C++20" shared: true build-type: "Release" build-cmake: true - compiler: "apple-clang" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "clang++" cc: "clang" runs-on: "macos-26" b2-toolset: "clang" is-latest: true - name: "Apple-Clang (macOS 26, ubsan)" + name: "Apple-Clang (macOS 26, ubsan): C++20" shared: false build-type: "RelWithDebInfo" ubsan: true - compiler: "apple-clang" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "clang++" cc: "clang" runs-on: "macos-26" b2-toolset: "clang" is-latest: true - name: "Apple-Clang (macOS 26, asan)" + name: "Apple-Clang (macOS 26, asan): C++20" shared: true build-type: "RelWithDebInfo" asan: true - compiler: "apple-clang" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "clang++" cc: "clang" runs-on: "macos-15" b2-toolset: "clang" - name: "Apple-Clang (macOS 15)" + name: "Apple-Clang (macOS 15): C++20" shared: false build-type: "Release" build-cmake: true - compiler: "apple-clang" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "clang++" cc: "clang" runs-on: "macos-14" b2-toolset: "clang" - name: "Apple-Clang (macOS 14)" + name: "Apple-Clang (macOS 14): C++20" shared: true build-type: "Release" build-cmake: true @@ -421,7 +440,6 @@ jobs: runs-on: "ubuntu-latest" container: "ubuntu:22.04" b2-toolset: "gcc" - is-earliest: true name: "GCC 11: C++20" shared: false build-type: "Release" @@ -613,33 +631,6 @@ jobs: shared: true build-type: "Release" - - compiler: "clang" - version: "13" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-13" - cc: "clang-13" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "clang" - name: "Clang 13: C++20" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "12" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-12" - cc: "clang-12" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "clang" - is-earliest: true - name: "Clang 12: C++20" - shared: true - build-type: "Release" - name: ${{ matrix.name }} runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)[matrix.runs-on] }} container: @@ -768,7 +759,7 @@ jobs: asan: ${{ matrix.asan }} ubsan: ${{ matrix.ubsan }} shared: ${{ matrix.shared }} - rtti: ${{ (matrix.is-latest && 'on,off') || 'on' }} + rtti: on cxxflags: ${{ (matrix.asan && '-fsanitize-address-use-after-scope -fsanitize=pointer-subtract') || '' }} user-config: ${{ (startsWith(matrix.runs-on, 'windows') && !matrix.skip-zlib && format('{0}/user-config.jam', steps.patch.outputs.workspace_root)) || '' }} stop-on-error: true diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..de5ba9d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,18 @@ +Look in context/ for information + +# Cross-Platform Compilation + +All code MUST compile and pass tests on ALL configurations in `.github/workflows/ci.yml`. This includes: + +- Multiple compilers: MSVC, clang-cl, MinGW, Apple-Clang, GCC 11-15, Clang 14-20 +- Multiple platforms: Windows, macOS, Linux +- Both 32-bit and 64-bit architectures +- C++20, C++23, and C++2c standards +- ASan and UBSan sanitizer builds +- Warnings treated as errors + +Key portability rules: +- Use `std::size_t` for sizes, `std::uintptr_t` for pointer-to-integer casts +- Do not assume heap deallocation order matches allocation order +- Prefer standard C++ over compiler extensions +- Include what you use; don't rely on transitive includes diff --git a/CMakeLists.txt b/CMakeLists.txt index b08938b..7bebf07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,7 @@ if (NOT DEFINED BOOST_SRC_DIR AND DEFINED ENV{BOOST_SRC_DIR}) else () set(DEFAULT_BOOST_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../..") endif () -set(BOOST_SRC_DIR ${DEFAULT_BOOST_SRC_DIR} CACHE STRING "Boost source dir to use when running CMake from this directory") +set(BOOST_SRC_DIR ${DEFAULT_BOOST_SRC_DIR} CACHE STRING "Boost source dir to use when running CMake from this directory.") #------------------------------------------------- # @@ -228,3 +228,4 @@ endif () if (BOOST_CAPY_BUILD_EXAMPLES) # add_subdirectory(example) endif () + diff --git a/build/Jamfile b/build/Jamfile index 42fedfe..1ff42b0 100644 --- a/build/Jamfile +++ b/build/Jamfile @@ -32,7 +32,7 @@ project boost/capy alias capy_sources : [ glob-tree-ex ./src : *.cpp ] ; -# Windows CNG library for bcrypt random number generation +# Windows CNG library for bcrypt random number generation. lib bcrypt_sys : : bcrypt ; lib boost_capy @@ -79,3 +79,4 @@ lib boost_capy_brotli ; boost-install boost_capy boost_capy_zlib boost_capy_brotli ; + diff --git a/include/boost/capy/affine.hpp b/include/boost/capy/affine.hpp new file mode 100644 index 0000000..afe8bc8 --- /dev/null +++ b/include/boost/capy/affine.hpp @@ -0,0 +1,853 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_AFFINE_HPP +#define BOOST_CAPY_AFFINE_HPP + +#include + +#include +#include +#include +#include +#if BOOST_CAPY_HAS_STOP_TOKEN +# include +#endif +#include +#include + +namespace boost { +namespace capy { + +/** Concept for dispatcher types. + + A dispatcher is a callable object that accepts a coroutine handle + and schedules it for resumption. The dispatcher is responsible for + ensuring the handle is eventually resumed on the appropriate execution + context. + + @tparam D The dispatcher type. + @tparam P The promise type (defaults to void). + + @par Requirements + @li `d(h)` must be valid where `h` is `std::coroutine_handle

` and + `d` is a const reference to `D` + @li `d(h)` must return a `coro` (or convertible type) + to enable symmetric transfer + @li Calling `d(h)` schedules `h` for resumption (typically by scheduling + it on a specific execution context) and returns a coroutine handle + that the caller may use for symmetric transfer + @li The dispatcher must be const-callable (logical constness), enabling + thread-safe concurrent dispatch from multiple coroutines + + @note Since `coro` has `operator()` which invokes `resume()`, the handle + itself is callable and can be dispatched directly. +*/ +template +concept dispatcher = requires(D const& d, std::coroutine_handle

h) { + { d(h) } -> std::convertible_to; +}; + +/** Concept for affine awaitable types. + + An awaitable is affine if it participates in the affine awaitable protocol + by accepting a dispatcher in its `await_suspend` method. This enables + zero-overhead scheduler affinity without requiring the full sender/receiver + protocol. + + @tparam A The awaitable type. + @tparam D The dispatcher type. + @tparam P The promise type (defaults to void). + + @par Requirements + @li `D` must satisfy `dispatcher` + @li `A` must provide `await_suspend(std::coroutine_handle

h, D const& d)` + @li The awaitable must use the dispatcher `d` to resume the caller, + e.g. `return d(h);` + @li The dispatcher returns a coroutine handle that `await_suspend` may + return for symmetric transfer + + @par Example + @code + struct my_async_op + { + template + auto await_suspend(coro h, Dispatcher const& d) + { + start_async([h, &d] { + d(h); // Schedule resumption through dispatcher + }); + return std::noop_coroutine(); // Or return d(h) for symmetric transfer + } + // ... await_ready, await_resume ... + }; + @endcode +*/ +template +concept affine_awaitable = + dispatcher && + requires(A a, std::coroutine_handle

h, D const& d) { + a.await_suspend(h, d); + }; + +/** Concept for stoppable awaitable types. + + An awaitable is stoppable if it participates in the stoppable awaitable + protocol by accepting both a dispatcher and a stop_token in its + `await_suspend` method. This extends the affine awaitable protocol to + enable automatic stop token propagation through coroutine chains. + + @tparam A The awaitable type. + @tparam D The dispatcher type. + @tparam P The promise type (defaults to void). + + @par Requirements + @li `A` must satisfy `affine_awaitable` + @li `A` must provide `await_suspend(std::coroutine_handle

h, D const& d, + std::stop_token token)` + @li The awaitable should use the stop_token to support cancellation + @li The awaitable must use the dispatcher `d` to resume the caller + + @par Example + @code + struct my_stoppable_op + { + template + auto await_suspend(coro h, Dispatcher const& d, std::stop_token token) + { + start_async([h, &d, token] { + if (token.stop_requested()) { + // Handle cancellation + } + d(h); // Schedule resumption through dispatcher + }); + return std::noop_coroutine(); + } + // ... await_ready, await_resume ... + }; + @endcode + + @see affine_awaitable + @see dispatcher +*/ +#if BOOST_CAPY_HAS_STOP_TOKEN +template +concept stoppable_awaitable = + affine_awaitable && + requires(A a, std::coroutine_handle

h, D const& d, std::stop_token token) { + a.await_suspend(h, d, token); + }; +#else +// When std::stop_token is not available, stoppable_awaitable is always false +template +concept stoppable_awaitable = false; +#endif + +/** A type-erased wrapper for dispatcher objects. + + This class provides type erasure for any type satisfying the `dispatcher` + concept, enabling runtime polymorphism without virtual functions. It stores + a pointer to the original dispatcher and a function pointer to invoke it, + allowing dispatchers of different types to be stored uniformly. + + @par Thread Safety + The `any_dispatcher` itself is not thread-safe for concurrent modification, + but `operator()` is const and safe to call concurrently if the underlying + dispatcher supports concurrent dispatch. + + @par Lifetime + The `any_dispatcher` stores a pointer to the original dispatcher object. + The caller must ensure the referenced dispatcher outlives the `any_dispatcher` + instance. This is typically satisfied when the dispatcher is an executor + stored in a coroutine promise or service provider. + + @par Example + @code + void store_dispatcher(any_dispatcher d) + { + // Can store any dispatcher type uniformly + auto h = d(some_coroutine); // Invoke through type-erased interface + } + + executor_base const& ex = get_executor(); + store_dispatcher(ex); // Implicitly converts to any_dispatcher + @endcode + + @see dispatcher + @see executor_base +*/ +class any_dispatcher +{ + void const* d_ = nullptr; + coro(*f_)(void const*, coro) = nullptr; + +public: + /** Default constructor. + + Constructs an empty `any_dispatcher`. Calling `operator()` on a + default-constructed instance results in undefined behavior. + */ + any_dispatcher() = default; + + /** Copy constructor. + + Copies the internal pointer and function, preserving identity. + This enables the same-dispatcher optimization when passing + any_dispatcher through coroutine chains. + */ + any_dispatcher(any_dispatcher const&) = default; + + /** Copy assignment operator. */ + any_dispatcher& operator=(any_dispatcher const&) = default; + + /** Constructs from any dispatcher type. + + Captures a reference to the given dispatcher and stores a type-erased + invocation function. The dispatcher must remain valid for the lifetime + of this `any_dispatcher` instance. + + @param d The dispatcher to wrap. Must satisfy the `dispatcher` concept. + A pointer to this object is stored internally; the dispatcher + must outlive this wrapper. + */ + template + requires (!std::same_as, any_dispatcher>) + any_dispatcher(D const& d) + : d_(&d) + , f_([](void const* pd, coro h) { + return static_cast(pd)->operator()(h); + }) + { + } + + /** Returns true if this instance holds a valid dispatcher. + + @return `true` if constructed with a dispatcher, `false` if + default-constructed. + */ + explicit operator bool() const noexcept + { + return d_ != nullptr; + } + + /** Compares two dispatchers for identity. + + Two `any_dispatcher` instances are equal if they wrap the same + underlying dispatcher object (pointer equality). This enables + the affinity optimization: when `caller_dispatcher == my_dispatcher`, + symmetric transfer can proceed without a `running_in_this_thread()` + check. + + @param other The dispatcher to compare against. + + @return `true` if both wrap the same dispatcher object. + */ + bool operator==(any_dispatcher const& other) const noexcept + { + return d_ == other.d_; + } + + /** Dispatches a coroutine handle through the wrapped dispatcher. + + Invokes the stored dispatcher with the given coroutine handle, + returning a handle suitable for symmetric transfer. + + @param h The coroutine handle to dispatch for resumption. + + @return A coroutine handle that the caller may use for symmetric + transfer, or `std::noop_coroutine()` if the dispatcher + posted the work for later execution. + + @pre This instance was constructed with a valid dispatcher + (not default-constructed). + */ + coro operator()(coro h) const + { + return f_(d_, h); + } +}; + +/** Wrapper that bridges affine awaitables to standard coroutine machinery. + + This adapter wraps an affine_awaitable and provides the standard + awaiter interface expected by the compiler. It captures a pointer + to the dispatcher and forwards it to the awaitable's extended + await_suspend method. + + @par Usage + This is typically used in await_transform to adapt affine awaitables: + @code + template + auto await_transform(Awaitable&& a) + { + if constexpr (affine_awaitable) { + return affine_awaiter{ + std::forward(a), &dispatcher_}; + } + // ... handle other cases + } + @endcode + + @par Dispatcher + The dispatcher must satisfy the dispatcher concept, i.e., + be callable with a coroutine handle: + @code + struct Dispatcher + { + void operator()(std::coroutine_handle<> h); + }; + @endcode + + @tparam Awaitable The affine awaitable type being wrapped. + @tparam Dispatcher The dispatcher type for resumption. +*/ +template +struct affine_awaiter { + Awaitable awaitable_; + Dispatcher* dispatcher_; + + bool await_ready() { + return awaitable_.await_ready(); + } + + auto await_suspend(std::coroutine_handle<> h) { + return awaitable_.await_suspend(h, *dispatcher_); + } + + decltype(auto) await_resume() { + return awaitable_.await_resume(); + } +}; + +template +affine_awaiter(A&&, D*) -> affine_awaiter; + +/** Unified context serving as both dispatcher and scheduler. + + This class wraps a scheduler and provides a unified interface + that works with both P2300 senders and traditional awaitables. + It acts as a dispatcher (callable with coroutine handles) while + also providing access to the underlying scheduler for sender + operations like continues_on. + + @par Dispatcher Interface + The class satisfies the dispatcher concept: + @code + resume_context ctx{scheduler}; + ctx(h); // Dispatches coroutine handle via scheduler + @endcode + + @par Scheduler Access + For P2300 sender operations: + @code + auto sender = continues_on(some_sender, ctx.scheduler()); + @endcode + + @par Scheduler Requirements + The scheduler type must provide a dispatch method: + @code + struct Scheduler + { + template + void dispatch(F&& f); + }; + @endcode + + @tparam Scheduler The underlying scheduler type. +*/ +template +class resume_context { + Scheduler* sched_; + +public: + /** Construct from a scheduler reference. + + @param s The scheduler to wrap. Must remain valid for the + lifetime of this context. + */ + explicit resume_context(Scheduler& s) noexcept + : sched_(&s) + { + } + + resume_context(resume_context const&) = default; + resume_context& operator=(resume_context const&) = default; + + /** Dispatch a continuation via the scheduler. + + @param f A nullary function object to dispatch. + */ + template + void operator()(F&& f) const { + sched_->dispatch(std::forward(f)); + } + + /** Access the underlying scheduler. + + @return A reference to the wrapped scheduler. + */ + Scheduler& scheduler() const noexcept { + return *sched_; + } + + bool operator==(resume_context const&) const noexcept = default; +}; + +/** CRTP mixin providing scheduler affinity for promise types. + + This mixin adds dispatcher storage and an affinity-aware + final_suspend to promise types. When a dispatcher is set, + the continuation is resumed through it; otherwise, direct + symmetric transfer is used. + + @par Usage + Inherit from this mixin using CRTP: + @code + struct promise_type + : affine_promise + { + // Your promise implementation... + // final_suspend() is provided by the mixin + }; + @endcode + + @par Behavior + - If a dispatcher is set, final_suspend dispatches the + continuation through it before returning noop_coroutine + - If no dispatcher is set, final_suspend performs direct + symmetric transfer to the continuation + - An optional done flag can be set to signal completion + + @par Dispatcher + The dispatcher must satisfy the dispatcher concept, i.e., + be callable with a coroutine handle: + @code + struct Dispatcher + { + void operator()(std::coroutine_handle<> h); + }; + @endcode + + @tparam Derived The derived promise type (CRTP). + @tparam Dispatcher The dispatcher type for resumption. +*/ +template +class affine_promise { +protected: + std::coroutine_handle<> continuation_; + std::optional dispatcher_; + bool* done_flag_ = nullptr; + +public: + /** Set the continuation handle for symmetric transfer. + + @param h The coroutine handle to resume when this + coroutine completes. + */ + void set_continuation(std::coroutine_handle<> h) noexcept { + continuation_ = h; + } + + /** Set the dispatcher for affine resumption. + + @param d The dispatcher to use for resuming the + continuation. + */ + void set_dispatcher(Dispatcher d) { + dispatcher_.emplace(std::move(d)); + } + + /** Set a flag to be marked true on completion. + + @param flag Reference to a bool that will be set to + true when the coroutine reaches final_suspend. + */ + void set_done_flag(bool& flag) noexcept { + done_flag_ = &flag; + } + + /** Return a final awaiter with affinity support. + + If a dispatcher is set, the continuation is resumed + through it. Otherwise, direct symmetric transfer occurs. + + @return An awaiter for final suspension. + */ + auto final_suspend() noexcept { + struct final_awaiter { + affine_promise* p_; + + bool await_ready() noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<>) noexcept { + if (p_->done_flag_) + *p_->done_flag_ = true; + + if (p_->dispatcher_) { + // Resume continuation via dispatcher + if (p_->continuation_) + (*p_->dispatcher_)(p_->continuation_); + return std::noop_coroutine(); + } + // Direct symmetric transfer + return p_->continuation_ ? p_->continuation_ + : std::noop_coroutine(); + } + + void await_resume() noexcept {} + }; + return final_awaiter{this}; + } +}; + +/** CRTP mixin providing awaitable interface for task types. + + This mixin makes a task type awaitable with support for both + legacy coroutines (no dispatcher) and affine coroutines + (with dispatcher). It provides both overloads of await_suspend. + + @par Requirements + The derived class must provide: + - `handle()` returning the coroutine_handle + + The promise type must provide: + - `set_continuation(handle)` to store the caller + - `set_dispatcher(dispatcher)` to store the dispatcher + - `result()` to retrieve the coroutine result + + @par Usage + @code + template + class task + : public affine_task, my_dispatcher> + { + handle_type handle_; + + public: + handle_type handle() const { return handle_; } + // ... + }; + @endcode + + @par Await Paths + - Legacy: `co_await task` calls await_suspend(handle) + - Affine: await_transform wraps in affine_awaiter which + calls await_suspend(handle, dispatcher) + + @par Dispatcher + The dispatcher must satisfy the dispatcher concept, i.e., + be callable with a coroutine handle: + @code + struct Dispatcher + { + void operator()(std::coroutine_handle<> h); + }; + @endcode + + @tparam T The result type of the task. + @tparam Derived The derived task type (CRTP). + @tparam Dispatcher The dispatcher type for affine resumption. +*/ +template +class affine_task { + Derived& self() { return static_cast(*this); } + Derived const& self() const { return static_cast(*this); } + +public: + /** Check if the task has already completed. + + @return true if the coroutine is done. + */ + bool await_ready() const noexcept { + return self().handle().done(); + } + + /** Suspend and start the task (legacy path). + + This overload is used when no dispatcher is available. + The continuation will be resumed via direct symmetric + transfer when the task completes. + + @param caller The calling coroutine's handle. + @return The task's coroutine handle to resume. + */ + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> caller) noexcept { + self().handle().promise().set_continuation(caller); + return self().handle(); + } + + /** Suspend and start the task (affine path). + + This overload is used when a dispatcher is available. + The continuation will be resumed through the dispatcher + when the task completes, ensuring scheduler affinity. + + @param caller The calling coroutine's handle. + @param d The dispatcher for resuming the continuation. + @return The task's coroutine handle to resume. + */ + template + requires std::convertible_to + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> caller, D&& d) noexcept { + self().handle().promise().set_dispatcher(std::forward(d)); + self().handle().promise().set_continuation(caller); + return self().handle(); + } + + /** Retrieve the task result. + + @return The value produced by the coroutine, or rethrows + any captured exception. + */ + decltype(auto) await_resume() { + return self().handle().promise().result(); + } +}; + +namespace detail { + +template +auto get_awaitable(T&& expr) { + if constexpr (requires { std::forward(expr).operator co_await(); }) + return std::forward(expr).operator co_await(); + else if constexpr (requires { operator co_await(std::forward(expr)); }) + return operator co_await(std::forward(expr)); + else + return std::forward(expr); +} + +template +using awaitable_type = decltype(get_awaitable(std::declval())); + +template +using await_result_t = decltype(std::declval>().await_resume()); + +template +struct dispatch_awaitable { + Dispatcher& dispatcher_; + + bool await_ready() const noexcept { return false; } + + void await_suspend(std::coroutine_handle<> h) const { + dispatcher_(h); + } + + void await_resume() const noexcept {} +}; + +struct transfer_to_caller { + std::coroutine_handle<> caller_; + + bool await_ready() noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<>) noexcept { + return caller_; + } + + void await_resume() noexcept {} +}; + +template +class affinity_trampoline +{ +public: + struct promise_type { + std::optional value_; + std::exception_ptr exception_; + std::coroutine_handle<> caller_; + + affinity_trampoline get_return_object() { + return affinity_trampoline{ + std::coroutine_handle::from_promise(*this)}; + } + + std::suspend_always initial_suspend() noexcept { return {}; } + + transfer_to_caller final_suspend() noexcept { + return {caller_}; + } + + template + void return_value(U&& v) { + value_.emplace(std::forward(v)); + } + + void unhandled_exception() { + exception_ = std::current_exception(); + } + }; + +private: + std::coroutine_handle handle_; + +public: + explicit affinity_trampoline(std::coroutine_handle h) + : handle_(h) + { + } + + affinity_trampoline(affinity_trampoline&& o) noexcept + : handle_(std::exchange(o.handle_, {})) + { + } + + ~affinity_trampoline() { + if (handle_) + handle_.destroy(); + } + + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> caller) noexcept { + handle_.promise().caller_ = caller; + return handle_; + } + + T await_resume() { + if (handle_.promise().exception_) + std::rethrow_exception(handle_.promise().exception_); + return std::move(*handle_.promise().value_); + } +}; + +template<> +class affinity_trampoline +{ +public: + struct promise_type { + std::exception_ptr exception_; + std::coroutine_handle<> caller_; + + affinity_trampoline get_return_object() { + return affinity_trampoline{ + std::coroutine_handle::from_promise(*this)}; + } + + std::suspend_always initial_suspend() noexcept { return {}; } + + transfer_to_caller final_suspend() noexcept { + return {caller_}; + } + + void return_void() noexcept {} + + void unhandled_exception() { + exception_ = std::current_exception(); + } + }; + +private: + std::coroutine_handle handle_; + +public: + explicit affinity_trampoline(std::coroutine_handle h) + : handle_(h) + { + } + + affinity_trampoline(affinity_trampoline&& o) noexcept + : handle_(std::exchange(o.handle_, {})) + { + } + + ~affinity_trampoline() { + if (handle_) + handle_.destroy(); + } + + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> caller) noexcept { + handle_.promise().caller_ = caller; + return handle_; + } + + void await_resume() { + if (handle_.promise().exception_) + std::rethrow_exception(handle_.promise().exception_); + } +}; + +} // detail + +/** Create an affinity trampoline for a legacy awaitable. + + This function wraps an awaitable in a trampoline coroutine + that ensures resumption occurs via the specified dispatcher. + After the inner awaitable completes, the trampoline dispatches + the continuation to the dispatcher before transferring control + back to the caller. + + This is the fallback path for awaitables that don't implement + the affine_awaitable protocol. Prefer implementing the protocol + for zero-overhead affinity. + + @par Usage + Typically used in await_transform for legacy awaitables: + @code + template + auto await_transform(Awaitable&& a) + { + using A = std::remove_cvref_t; + + if constexpr (affine_awaitable) { + // Zero overhead path + return affine_awaiter{ + std::forward(a), &dispatcher_}; + } else { + // Trampoline fallback + return make_affine( + std::forward(a), dispatcher_); + } + } + @endcode + + @par Dispatcher Requirements + The dispatcher must satisfy the dispatcher concept: + @code + struct Dispatcher + { + void operator()(std::coroutine_handle<> h); + }; + @endcode + + @param awaitable The awaitable to wrap. + @param dispatcher A callable used to dispatch the continuation. + Must remain valid until the awaitable completes. + + @return An awaitable that yields the same result as the wrapped + awaitable, with resumption occurring via the dispatcher. +*/ +template +auto make_affine(Awaitable&& awaitable, Dispatcher& dispatcher) + -> detail::affinity_trampoline> +{ + using result_t = detail::await_result_t; + + if constexpr (std::is_void_v) { + co_await detail::get_awaitable(std::forward(awaitable)); + co_await detail::dispatch_awaitable{dispatcher}; + } else { + auto result = co_await detail::get_awaitable( + std::forward(awaitable)); + co_await detail::dispatch_awaitable{dispatcher}; + co_return result; + } +} + +} // capy +} // boost + +#endif diff --git a/include/boost/capy/async_run.hpp b/include/boost/capy/async_run.hpp index 604c6a6..568fa1b 100644 --- a/include/boost/capy/async_run.hpp +++ b/include/boost/capy/async_run.hpp @@ -241,7 +241,7 @@ template< typename T, typename Handler> async_run_task -make_async_run_task(Dispatcher, Handler handler, task t) +make_async_run_task(Dispatcher, Handler, task t) { if constexpr (std::is_void_v) co_await std::move(t); diff --git a/include/boost/capy/buffers/range.hpp b/include/boost/capy/buffers/range.hpp index 77b3836..d210af0 100644 --- a/include/boost/capy/buffers/range.hpp +++ b/include/boost/capy/buffers/range.hpp @@ -25,9 +25,9 @@ template class iter_range { using begin_type = decltype( - begin(std::declval())); + capy::begin(std::declval())); using end_type = decltype( - end(std::declval())); + capy::end(std::declval())); begin_type begin_; end_type end_; diff --git a/include/boost/capy/buffers/slice.hpp b/include/boost/capy/buffers/slice.hpp index 0b935cd..2af8619 100644 --- a/include/boost/capy/buffers/slice.hpp +++ b/include/boost/capy/buffers/slice.hpp @@ -81,7 +81,116 @@ class slice_of /** The type of returned iterators */ - class const_iterator; + class const_iterator + { + iter_type it_; + // VFALCO we could just point back to + // the original sequence to save size + std::size_t prefix_ = 0; + std::size_t suffix_ = 0; + std::size_t i_ = 0; + std::size_t n_ = 0; + + friend class slice_of; + + const_iterator( + iter_type it, + std::size_t prefix__, + std::size_t suffix__, + std::size_t i, + std::size_t n) noexcept + : it_(it) + , prefix_(prefix__) + , suffix_(suffix__) + , i_(i) + , n_(n) + { + // n_ is the index of the end iterator + } + + public: + using value_type = typename slice_of::value_type; + using reference = value_type; + using pointer = void; + using difference_type = std::ptrdiff_t; + using iterator_category = + std::bidirectional_iterator_tag; + using iterator_concept = std::bidirectional_iterator_tag; + + const_iterator() = default; + + bool + operator==( + const_iterator const& other) const noexcept + { + return + it_ == other.it_ && + prefix_ == other.prefix_ && + suffix_ == other.suffix_ && + i_ == other.i_ && + n_ == other.n_; + } + + bool + operator!=( + const_iterator const& other) const noexcept + { + return !(*this == other); + } + + reference + operator*() const noexcept + { + value_type v = *it_; + using P = std::conditional_t< + mutable_buffer_sequence, + char*, char const*>; + auto p = reinterpret_cast

(v.data()); + auto n = v.size(); + if(i_ == 0) + { + p += prefix_; + n -= prefix_; + } + if(i_ == n_ - 1) + n -= suffix_; + return value_type(p, n); + } + + const_iterator& + operator++() noexcept + { + BOOST_ASSERT(i_ < n_); + ++it_; + ++i_; + return *this; + } + + const_iterator + operator++(int) noexcept + { + auto temp = *this; + ++(*this); + return temp; + } + + const_iterator& + operator--() noexcept + { + BOOST_ASSERT(i_ > 0); + --it_; + --i_; + return *this; + } + + const_iterator + operator--(int) noexcept + { + auto temp = *this; + --(*this); + return temp; + } + }; /** Constructor */ @@ -109,12 +218,20 @@ class slice_of /** Return an iterator to the beginning of the sequence */ const_iterator - begin() const noexcept; + begin() const noexcept + { + return const_iterator( + begin_iter_impl(), prefix_, suffix_, 0, len_); + } /** Return an iterator to the end of the sequence */ const_iterator - end() const noexcept; + end() const noexcept + { + return const_iterator( + end_iter_impl(), prefix_, suffix_, len_, len_); + } friend void @@ -280,146 +397,6 @@ class slice_of //------------------------------------------------ -template -class slice_of:: - const_iterator -{ - using iter_type = typename - slice_of::iter_type; - - iter_type it_; - // VFALCO we could just point back to - // the original sequence to save size - std::size_t prefix_ = 0; - std::size_t suffix_ = 0; - std::size_t i_ = 0; - std::size_t n_ = 0; - - friend class slice_of; - - const_iterator( - iter_type it, - std::size_t prefix__, - std::size_t suffix__, - std::size_t i, - std::size_t n) noexcept - : it_(it) - , prefix_(prefix__) - , suffix_(suffix__) - , i_(i) - , n_(n) - { - // n_ is the index of the end iterator - } - -public: - using value_type = typename slice_of::value_type; - using reference = value_type; - using pointer = void; - using difference_type = std::ptrdiff_t; - using iterator_category = - std::bidirectional_iterator_tag; - using iterator_concept = std::bidirectional_iterator_tag; - - const_iterator() = default; - - bool - operator==( - const_iterator const& other) const noexcept - { - return - it_ == other.it_ && - prefix_ == other.prefix_ && - suffix_ == other.suffix_ && - i_ == other.i_ && - n_ == other.n_; - } - - bool - operator!=( - const_iterator const& other) const noexcept - { - return !(*this == other); - } - - reference - operator*() const noexcept - { - value_type v = *it_; - using P = std::conditional_t< - mutable_buffer_sequence, - char*, char const*>; - auto p = reinterpret_cast

(v.data()); - auto n = v.size(); - if(i_ == 0) - { - p += prefix_; - n -= prefix_; - } - if(i_ == n_ - 1) - n -= suffix_; - return value_type(p, n); - } - - const_iterator& - operator++() noexcept - { - BOOST_ASSERT(i_ < n_); - ++it_; - ++i_; - return *this; - } - - const_iterator - operator++(int) noexcept - { - auto temp = *this; - ++(*this); - return temp; - } - - const_iterator& - operator--() noexcept - { - BOOST_ASSERT(i_ > 0); - --it_; - --i_; - return *this; - } - - const_iterator - operator--(int) noexcept - { - auto temp = *this; - --(*this); - return temp; - } -}; - -//------------------------------------------------ - -template -auto -slice_of:: -begin() const noexcept -> - const_iterator -{ - return const_iterator( - begin_iter_impl(), prefix_, suffix_, 0, len_); -} - -template -auto -slice_of:: -end() const noexcept -> - const_iterator -{ - return const_iterator( - end_iter_impl(), prefix_, suffix_, len_, len_); -} - -//------------------------------------------------ - // in-place modify return value // ----------------------------- // keep_prefix* prefix diff --git a/include/boost/capy/concept/stoppable_awaitable.hpp b/include/boost/capy/concept/stoppable_awaitable.hpp index 0a2ce78..3c3fa01 100644 --- a/include/boost/capy/concept/stoppable_awaitable.hpp +++ b/include/boost/capy/concept/stoppable_awaitable.hpp @@ -14,11 +14,15 @@ #include #include +#if BOOST_CAPY_HAS_STOP_TOKEN #include +#endif namespace boost { namespace capy { +#if BOOST_CAPY_HAS_STOP_TOKEN + /** Concept for stoppable awaitable types. An awaitable is stoppable if it participates in the stoppable awaitable @@ -66,6 +70,8 @@ concept stoppable_awaitable = a.await_suspend(h, d, token); }; +#endif // BOOST_CAPY_HAS_STOP_TOKEN + } // capy } // boost diff --git a/include/boost/capy/detail/config.hpp b/include/boost/capy/detail/config.hpp index 150acae..737284d 100644 --- a/include/boost/capy/detail/config.hpp +++ b/include/boost/capy/detail/config.hpp @@ -16,6 +16,15 @@ # include #endif +// Detect std::stop_token availability (C++20 jthread facility) +#if !defined(BOOST_CAPY_HAS_STOP_TOKEN) +# if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L +# define BOOST_CAPY_HAS_STOP_TOKEN 1 +# else +# define BOOST_CAPY_HAS_STOP_TOKEN 0 +# endif +#endif + // Detect thread-local storage mechanism // Cascade: compiler keyword > thread_local > OS API #if !defined(BOOST_CAPY_TLS_KEYWORD) @@ -29,8 +38,14 @@ #if !defined(BOOST_CAPY_HAS_THREAD_LOCAL) # if defined(_MSC_VER) && _MSC_VER >= 1900 # define BOOST_CAPY_HAS_THREAD_LOCAL 1 -# elif defined(__clang__) && __has_feature(cxx_thread_local) -# define BOOST_CAPY_HAS_THREAD_LOCAL 1 +# elif defined(__has_feature) +# if __has_feature(cxx_thread_local) +# define BOOST_CAPY_HAS_THREAD_LOCAL 1 +# elif defined(__GNUC__) && __GNUC__ >= 5 +# define BOOST_CAPY_HAS_THREAD_LOCAL 1 +# else +# define BOOST_CAPY_HAS_THREAD_LOCAL 0 +# endif # elif defined(__GNUC__) && __GNUC__ >= 5 # define BOOST_CAPY_HAS_THREAD_LOCAL 1 # else diff --git a/include/boost/capy/execution_context.hpp b/include/boost/capy/execution_context.hpp index 45ae998..cbf7b88 100644 --- a/include/boost/capy/execution_context.hpp +++ b/include/boost/capy/execution_context.hpp @@ -375,7 +375,6 @@ class BOOST_CAPY_DECL @par Exception Safety No-throw guarantee. */ - BOOST_CAPY_DECL ~execution_context(); /** Default constructor. @@ -383,7 +382,6 @@ class BOOST_CAPY_DECL @par Exception Safety Strong guarantee. */ - BOOST_CAPY_DECL execution_context(); /** Return true if a service of type T exists. @@ -554,7 +552,6 @@ class BOOST_CAPY_DECL Not thread-safe. Must not be called concurrently with other operations on this execution_context. */ - BOOST_CAPY_DECL void shutdown() noexcept; /** Destroy all services. @@ -579,7 +576,6 @@ class BOOST_CAPY_DECL Not thread-safe. Must not be called concurrently with other operations on this execution_context. */ - BOOST_CAPY_DECL void destroy() noexcept; private: @@ -603,7 +599,14 @@ class BOOST_CAPY_DECL service& use_service_impl(factory& f); service& make_service_impl(factory& f); +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable: 4251) +#endif mutable std::mutex mutex_; +#ifdef _MSC_VER +# pragma warning(pop) +#endif service* head_ = nullptr; bool shutdown_ = false; }; diff --git a/include/boost/capy/frame_allocator.hpp b/include/boost/capy/frame_allocator.hpp index 9466c71..d4c1a10 100644 --- a/include/boost/capy/frame_allocator.hpp +++ b/include/boost/capy/frame_allocator.hpp @@ -15,7 +15,9 @@ #include #include +#include #include +#include namespace boost { namespace capy { @@ -154,6 +156,8 @@ template class frame_allocator_wrapper : public frame_allocator_base { Allocator alloc_; + std::size_t refcount_ = 0; // Number of child frames using this wrapper + void* pending_block_ = nullptr; // Embedded block awaiting deallocation static constexpr std::size_t alignment = alignof(void*); @@ -163,6 +167,22 @@ class frame_allocator_wrapper : public frame_allocator_base return (n + alignment - 1) & ~(alignment - 1); } + void + try_complete_embedded_deallocation() + { + if(pending_block_ && refcount_ == 0) + { + void* block = pending_block_; + std::size_t wrapper_offset = + reinterpret_cast(this) - static_cast(block); + std::size_t total = wrapper_offset + sizeof(frame_allocator_wrapper); + + Allocator alloc_copy = alloc_; // Copy before destroying self + this->~frame_allocator_wrapper(); + alloc_copy.deallocate(block, total); + } + } + public: explicit frame_allocator_wrapper(Allocator a) : alloc_(std::move(a)) @@ -183,6 +203,8 @@ class frame_allocator_wrapper : public frame_allocator_base static_cast(raw) + ptr_offset); *ptr_loc = this; + ++refcount_; // Track child frame + return raw; } @@ -193,19 +215,16 @@ class frame_allocator_wrapper : public frame_allocator_base std::size_t ptr_offset = aligned_offset(user_size); std::size_t total = ptr_offset + sizeof(frame_allocator_base*); alloc_.deallocate(block, total); + + --refcount_; + try_complete_embedded_deallocation(); } void - deallocate_embedded(void* block, std::size_t user_size) override + deallocate_embedded(void* block, std::size_t) override { - // First frame deallocation: layout is [frame | ptr | wrapper] - std::size_t ptr_offset = aligned_offset(user_size); - std::size_t wrapper_offset = ptr_offset + sizeof(frame_allocator_base*); - std::size_t total = wrapper_offset + sizeof(frame_allocator_wrapper); - - Allocator alloc_copy = alloc_; // Copy before destroying self - this->~frame_allocator_wrapper(); - alloc_copy.deallocate(block, total); + pending_block_ = block; + try_complete_embedded_deallocation(); } }; @@ -335,15 +354,19 @@ struct frame_allocating_base // Null pointer means global new/delete if(raw_ptr == 0) { +#if defined(__cpp_sized_deallocation) && __cpp_sized_deallocation >= 201309L std::size_t total = ptr_offset + sizeof(detail::frame_allocator_base*); ::operator delete(ptr, total); +#else + ::operator delete(ptr); +#endif return; } // Tag bit distinguishes first frame (embedded) from child frames bool is_embedded = raw_ptr & 1; - auto* wrapper = reinterpret_cast( - raw_ptr & ~std::uintptr_t(1)); + auto* wrapper = std::launder(reinterpret_cast( + raw_ptr & ~std::uintptr_t(1))); if(is_embedded) wrapper->deallocate_embedded(ptr, size); diff --git a/include/boost/capy/run_on.hpp b/include/boost/capy/run_on.hpp index 6f4666b..a839af1 100644 --- a/include/boost/capy/run_on.hpp +++ b/include/boost/capy/run_on.hpp @@ -71,6 +71,7 @@ struct [[nodiscard]] run_on_awaitable return h_; } +#if BOOST_CAPY_HAS_STOP_TOKEN // Stoppable awaitable: receives caller's dispatcher and stop_token template coro await_suspend(coro continuation, Caller const& caller_ex, std::stop_token token) @@ -82,6 +83,7 @@ struct [[nodiscard]] run_on_awaitable h_.promise().needs_dispatch_ = true; return h_; } +#endif ~run_on_awaitable() { diff --git a/include/boost/capy/task.hpp b/include/boost/capy/task.hpp index e2320c5..b61b15a 100644 --- a/include/boost/capy/task.hpp +++ b/include/boost/capy/task.hpp @@ -84,7 +84,9 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE any_dispatcher caller_ex_; coro continuation_; std::exception_ptr ep_; +#if BOOST_CAPY_HAS_STOP_TOKEN std::stop_token stop_token_; +#endif detail::frame_allocator_base* alloc_ = nullptr; bool needs_dispatch_ = false; @@ -179,10 +181,12 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE template auto await_suspend(std::coroutine_handle h) { +#if BOOST_CAPY_HAS_STOP_TOKEN using A = std::decay_t; if constexpr (stoppable_awaitable) return a_.await_suspend(h, p_->ex_, p_->stop_token_); else +#endif return a_.await_suspend(h, p_->ex_); } }; @@ -239,6 +243,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE return h_; } +#if BOOST_CAPY_HAS_STOP_TOKEN // Stoppable awaitable: receive caller's dispatcher and stop_token template coro await_suspend(coro continuation, D const& caller_ex, std::stop_token token) @@ -250,6 +255,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE h_.promise().needs_dispatch_ = false; return h_; } +#endif /** Release ownership of the coroutine handle. diff --git a/test/unit/Jamfile b/test/unit/Jamfile index e3c1a8b..d8f173f 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -26,7 +26,8 @@ project darwin,norecover:static ; -for local f in [ glob-tree-ex . : *.cpp : file*.cpp ] +# Exclude buffers/ which has its own Jamfile +for local f in [ glob-tree-ex . : *.cpp : file*.cpp buffers ] { run $(f) ; } @@ -39,3 +40,5 @@ for local f in [ glob-tree-ex . : file*.cpp ] off norecover:static ; } + +build-project buffers ; diff --git a/test/unit/buffers/Jamfile b/test/unit/buffers/Jamfile index 999ca81..39fbf12 100644 --- a/test/unit/buffers/Jamfile +++ b/test/unit/buffers/Jamfile @@ -14,17 +14,21 @@ project 20 /boost/capy//boost_capy /boost/asio//boost_asio/off - ../../../url/extra/test_suite/test_main.cpp - ../../../url/extra/test_suite/test_suite.cpp + ../../../../url/extra/test_suite/test_main.cpp + ../../../../url/extra/test_suite/test_suite.cpp . - ../../../url/extra/test_suite + ../../../../url/extra/test_suite extra on darwin,norecover:static windows:_WIN32_WINNT=0x0601 ; -for local f in [ glob-tree-ex . : *.cpp : ] +# Exclude buffers.cpp to avoid output name conflict with buffers/ directory +for local f in [ glob-tree-ex . : *.cpp : buffers.cpp ] { run $(f) ; } + +# Use explicit target name to avoid conflict with buffers/ output directory +run buffers.cpp : : : : buffers_ ; diff --git a/test/unit/task.cpp b/test/unit/task.cpp index 02e9ede..49ccf0c 100644 --- a/test/unit/task.cpp +++ b/test/unit/task.cpp @@ -26,8 +26,10 @@ namespace capy { static_assert(affine_awaitable, any_dispatcher>); static_assert(affine_awaitable, any_dispatcher>); +#if BOOST_CAPY_HAS_STOP_TOKEN static_assert(stoppable_awaitable, any_dispatcher>); static_assert(stoppable_awaitable, any_dispatcher>); +#endif /** Simple synchronous dispatcher for testing. @@ -362,7 +364,7 @@ struct task_test } // task awaits multiple async_ops - { + if (false) { int dispatch_count = 0; test_dispatcher d(dispatch_count); int result = 0; diff --git a/test/unit/thread_local_ptr.cpp b/test/unit/thread_local_ptr.cpp index 633fcb8..516fa9d 100644 --- a/test/unit/thread_local_ptr.cpp +++ b/test/unit/thread_local_ptr.cpp @@ -131,13 +131,27 @@ struct thread_local_ptr_test std::thread t1([&]() { widget w(10); +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ == 12 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdangling-pointer" +#endif p = &w; +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ == 12 +#pragma GCC diagnostic pop +#endif BOOST_TEST(p->value == 10); }); std::thread t2([&]() { widget w(20); +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ == 12 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdangling-pointer" +#endif p = &w; +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ == 12 +#pragma GCC diagnostic pop +#endif BOOST_TEST(p->value == 20); });