diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5a72dcc..5eaac8e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -56,17 +56,6 @@ jobs: sudo apt-get update sudo apt-get install -y ninja-build gcc g++ ccache cmake doxygen graphviz python3-all - - name: Cache Python dependencies - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: pip-${{ runner.os }}-${{ hashFiles('.github/workflows/docs.yml') }} - restore-keys: | - pip-${{ runner.os }}- - - - name: Install Python dependencies - run: pip install gcovr==7.0 - - name: Create cache directories run: mkdir -p ~/.ccache ~/.cache/CPM @@ -74,9 +63,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.ccache - key: ccache-${{ runner.os }}-${{ hashFiles('**/*.cpp', '**/*.hpp', '**/*.h', '**/*.c') }} + key: cppnet-ccache-${{ runner.os }}-${{ hashFiles('**/*.cpp', '**/*.hpp', '**/*.h', '**/*.c') }} restore-keys: | - ccache-${{ runner.os }}- + cppnet-ccache-${{ runner.os }}- - name: Cache CMake build directory uses: actions/cache@v4 @@ -85,22 +74,23 @@ jobs: build/debug/_deps build/debug/CMakeCache.txt build/debug/CMakeFiles - key: cmake-tests-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt', 'CMakePresets.json') }} + key: cppnet-cmake-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt', 'CMakePresets.json') }} restore-keys: | - cmake-tests-${{ runner.os }}- + cppnet-cmake-${{ runner.os }}- - name: Cache CPM sources. uses: actions/cache@v4 with: path: ~/.cache/CPM - key: cpm-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt') }} + key: cppnet-cpm-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt') }} restore-keys: | - cpm-${{ runner.os }}- + cppnet-cpm-${{ runner.os }}- - name: Configure CMake run: | cmake --preset debug \ -DCPM_SOURCE_CACHE='~/.cache/CPM' \ + -DCPPNET_BUILD_TESTING='OFF' \ -DCPPNET_BUILD_DOCS='ON' - name: Build documentation diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f24e0b..2f40154 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,9 +26,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/pip - key: pip-${{ runner.os }}-${{ hashFiles('.github/workflows/tests.yml') }} + key: cppnet-pip-${{ runner.os }}-${{ hashFiles('.github/workflows/tests.yml') }} restore-keys: | - pip-${{ runner.os }}- + cppnet-pip-${{ runner.os }}- - name: Install Python dependencies run: pip install gcovr @@ -40,9 +40,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.ccache - key: ccache-${{ runner.os }}-${{ hashFiles('**/*.cpp', '**/*.hpp', '**/*.h', '**/*.c') }} + key: cppnet-ccache-${{ runner.os }}-${{ hashFiles('**/*.cpp', '**/*.hpp', '**/*.h', '**/*.c') }} restore-keys: | - ccache-${{ runner.os }}- + cppnet-ccache-${{ runner.os }}- - name: Cache CMake build directory uses: actions/cache@v4 @@ -51,17 +51,17 @@ jobs: build/debug/_deps build/debug/CMakeCache.txt build/debug/CMakeFiles - key: cmake-tests-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt', 'CMakePresets.json') }} + key: cppnet-cmake-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt', 'CMakePresets.json') }} restore-keys: | - cmake-tests-${{ runner.os }}- + cppnet-cmake-${{ runner.os }}- - name: Cache CPM sources. uses: actions/cache@v4 with: path: ~/.cache/CPM - key: cpm-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt') }} + key: cppnet-cpm-${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt') }} restore-keys: | - cpm-${{ runner.os }}- + cppnet-cpm-${{ runner.os }}- - name: Configure ccache run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 9588a6c..ead4bcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.28) project( CppNet - VERSION 0.6.1 + VERSION 0.7.6 LANGUAGES CXX) # net requires at least C++20 diff --git a/include/net/cppnet.hpp b/include/net/cppnet.hpp index 5b7d41d..3f5242c 100644 --- a/include/net/cppnet.hpp +++ b/include/net/cppnet.hpp @@ -22,7 +22,10 @@ #define CPPNET_HPP /** @brief This is the root namespace of cppnet. */ namespace net {} // namespace net +#include "service/async_context.hpp" // IWYU pragma: export #include "service/async_tcp_service.hpp" // IWYU pragma: export #include "service/async_udp_service.hpp" // IWYU pragma: export #include "service/context_thread.hpp" // IWYU pragma: export +#include "timers/interrupt.hpp" // IWYU pragma: export +#include "timers/timers.hpp" // IWYU pragma: export #endif // CPPNET_HPP diff --git a/include/net/service/async_context.hpp b/include/net/service/async_context.hpp new file mode 100644 index 0000000..fc6edd5 --- /dev/null +++ b/include/net/service/async_context.hpp @@ -0,0 +1,118 @@ +/* Copyright (C) 2025 Kevin Exton (kevin.exton@pm.me) + * + * cppnet is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * cppnet is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with cppnet. If not, see . + */ + +/** + * @file async_context.hpp + * @brief This file declares an asynchronous execution context. + */ +#pragma once +#ifndef CPPNET_ASYNC_CONTEXT_HPP +#define CPPNET_ASYNC_CONTEXT_HPP +#include "net/detail/immovable.hpp" +#include "net/timers/timers.hpp" + +#include +#include + +#include +#include +/** @brief This namespace is for network services. */ +namespace net::service { + +/** @brief An asynchronous execution context. */ +struct async_context : detail::immovable { + /** @brief Asynchronous scope type. */ + using async_scope = exec::async_scope; + /** @brief The io multiplexer type. */ + using multiplexer_type = io::execution::poll_multiplexer; + /** @brief The io triggers type. */ + using triggers = io::execution::basic_triggers; + /** @brief The socket dialog type. */ + using socket_dialog = triggers::socket_dialog; + /** @brief The socket type. */ + using socket_type = io::socket::native_socket_type; + /** @brief The signal mask type. */ + using signal_mask = std::uint64_t; + /** @brief Interrupt source type. */ + using interrupt_source = timers::socketpair_interrupt_source_t; + /** @brief The timers type. */ + using timers_type = timers::timers; + /** @brief The clock type. */ + using clock = std::chrono::steady_clock; + /** @brief The duration type. */ + using duration = std::chrono::milliseconds; + + /** @brief An enum of all valid async context signals. */ + enum signals : std::uint8_t { terminate = 0, user1, END }; + /** @brief An enum of valid context states. */ + enum context_states : std::uint8_t { PENDING = 0, STARTED, STOPPED }; + + /** @brief The event loop timers. */ + timers_type timers; + /** @brief The asynchronous scope. */ + async_scope scope; + /** @brief The poll triggers. */ + triggers poller; + /** @brief The active signal mask. */ + std::atomic sigmask; + /** @brief A counter that tracks the context state. */ + std::atomic state{PENDING}; + + /** + * @brief Sets the signal mask, then interrupts the service. + * @param signum The signal to send. Must be in range of + * enum signals. + */ + inline auto signal(int signum) -> void; + + /** @brief Calls the timers interrupt. */ + inline auto interrupt() const noexcept -> void; + + /** + * @brief An interrupt service routine for the poller. + * @details When invoked, `isr()` installs an event + * handler on socket events received on `socket`. The routine will be + * continuously re-installed in a loop until it returns false. + * @tparam Fn A callable to run upon receiving an interrupt. + * @param socket The listening socket for interrupts. Its lifetime is + * tied to the lifetime of `routine`. + * @param routine The routine to run upon receiving a poll interrupt on + * `socket`. + * @code + * isr(poller.emplace(sockets[0]), [&]() noexcept { + * auto sigmask_ = sigmask.exchange(0); + * for (int signum = 0; auto mask = (sigmask_ >> signum); ++signum) + * { + * if (mask & (1 << 0)) + * service.signal_handler(signum); + * } + * return !(sigmask_ & (1 << terminate)); + * }); + * @endcode + */ + template + requires std::is_invocable_r_v + auto isr(const socket_dialog &socket, Fn routine) -> void; + + /** @brief Runs the event loop. */ + inline auto run() -> void; +}; + +} // namespace net::service + +#include "impl/async_context_impl.hpp" // IWYU pragma: export + +#endif // CPPNET_ASYNC_CONTEXT_HPP diff --git a/include/net/service/context_thread.hpp b/include/net/service/context_thread.hpp index d4f3b40..930ed5a 100644 --- a/include/net/service/context_thread.hpp +++ b/include/net/service/context_thread.hpp @@ -20,63 +20,12 @@ #pragma once #ifndef CPPNET_CONTEXT_THREAD_HPP #define CPPNET_CONTEXT_THREAD_HPP -#include "net/detail/concepts.hpp" -#include "net/detail/immovable.hpp" -#include "net/timers/timers.hpp" +#include "async_context.hpp" -#include -#include - -#include -#include #include #include -#include /** @brief This namespace is for network services. */ namespace net::service { - -/** @brief Data members for an asynchronous service context. */ -struct async_context : detail::immovable { - /** @brief Asynchronous scope type. */ - using async_scope = exec::async_scope; - /** @brief The io multiplexer type. */ - using multiplexer_type = io::execution::poll_multiplexer; - /** @brief The io triggers type. */ - using triggers = io::execution::basic_triggers; - /** @brief The signal mask type. */ - using signal_mask = std::uint64_t; - /** @brief Interrupt source type. */ - using interrupt_source = timers::socketpair_interrupt_source_t; - /** @brief The timers type. */ - using timers_type = timers::timers; - - /** @brief An enum of all valid async context signals. */ - enum signals : std::uint8_t { terminate = 0, user1, END }; - /** @brief An enum of valid context states. */ - enum context_states : std::uint8_t { PENDING = 0, STARTED, STOPPED }; - - /** @brief The asynchronous scope. */ - async_scope scope; - /** @brief The poll triggers. */ - triggers poller; - /** @brief A counter that tracks the context state. */ - std::atomic state{PENDING}; - /** @brief The active signal mask. */ - std::atomic sigmask; - /** @brief The event loop timers. */ - timers_type timers; - - /** - * @brief Sets the signal mask, then interrupts the service. - * @param signum The signal to send. Must be in range of - * enum signals. - */ - inline auto signal(int signum) -> void; - - /** @brief Calls the timers interrupt. */ - inline auto interrupt() const noexcept -> void; -}; - /** * @brief A threaded asynchronous service. * @@ -86,33 +35,6 @@ struct async_context : detail::immovable { * @tparam Service The service to run. */ template class context_thread : public async_context { - /** @brief The socket dialog type. */ - using socket_dialog = triggers::socket_dialog; - /** @brief The socket type. */ - using socket_type = io::socket::native_socket_type; - /** @brief The clock type. */ - using clock = std::chrono::steady_clock; - /** @brief The duration type. */ - using duration = std::chrono::milliseconds; - /** - * @brief An interrupt service routine. - * - * This interrupts the running event loop in a thread so - * that signals can be handled. - * - * @param scope The asynchronous scope. - * @param socket The listening socket for interrupts. - * @param handle A function that passes signals to the service - * signal handler. - */ - template - requires std::is_invocable_r_v - static auto isr(async_scope &scope, const socket_dialog &socket, - Fn handle) -> void; - - /** @brief Called when the async_service is stopped. */ - auto stop() noexcept -> void; - public: /** @brief Default constructor. */ context_thread() = default; @@ -145,19 +67,12 @@ template class context_thread : public async_context { /** @brief Flag that guards against starting a thread twice. */ bool started_{false}; - /** - * @brief Runs the event loop. - * @tparam StopToken The stop token type. - * @param service The service to run on the event loop. - * @param token The stop token to use with the service. - */ - template - auto run(Service &service, const StopToken &token) -> void; + /** @brief Called when the async_service is stopped. */ + auto stop() noexcept -> void; }; } // namespace net::service -#include "impl/async_context_impl.hpp" // IWYU pragma: export #include "impl/context_thread_impl.hpp" // IWYU pragma: export #endif // CPPNET_CONTEXT_THREAD_HPP diff --git a/include/net/service/impl/async_context_impl.hpp b/include/net/service/impl/async_context_impl.hpp index 432d36d..6435730 100644 --- a/include/net/service/impl/async_context_impl.hpp +++ b/include/net/service/impl/async_context_impl.hpp @@ -21,8 +21,35 @@ #pragma once #ifndef CPPNET_ASYNC_CONTEXT_IMPL_HPP #define CPPNET_ASYNC_CONTEXT_IMPL_HPP -#include "net/service/context_thread.hpp" +#include "net/service/async_context.hpp" + +#include namespace net::service { +/** @brief Internal net::service implementation details. */ +namespace detail { +/** + * @brief Computes the time in ms poller.wait_for() should block for + * before waking for the next event. + * @note This helper method was primarily added for the purposes + * of test coverage, it isn't really reusable outside of the + * async_context class. + * @tparam Rep The duration tick type. + * @tparam Period the tick period. + * @param duration The duration to convert to milliseconds. + * Must be in the range of [-1, duration.max()]. + * @returns -1 If the duration is -1, otherwise it returns the + * duration count in milliseconds. + */ +template > +auto to_millis(std::chrono::duration duration) -> int +{ + using namespace std::chrono; + assert(duration.count() >= -1 && + "duration must be in the interval of -1 to duration.max()"); + return (duration.count() < 0) ? duration.count() + : duration_cast(duration).count(); +} +} // namespace detail. inline auto async_context::signal(int signum) -> void { @@ -37,5 +64,41 @@ inline auto async_context::interrupt() const noexcept -> void static_cast(timers).interrupt(); } +template + requires std::is_invocable_r_v +auto async_context::isr(const socket_dialog &socket, Fn routine) -> void +{ + using namespace io::socket; + using namespace stdexec; + using socket_message = socket_message; + + static constexpr auto BUFLEN = 1024UL; + static auto buffer = std::array{}; + static auto msg = socket_message{.buffers = buffer}; + + if (!routine()) + return; + + sender auto recvmsg = io::recvmsg(socket, msg, 0) | + then([this, socket, func = std::move(routine)](auto) { + isr(socket, std::move(func)); + }) | + upon_error([](auto) noexcept {}); + scope.spawn(std::move(recvmsg)); +} + +inline auto async_context::run() -> void +{ + using namespace stdexec; + using namespace std::chrono; + using namespace detail; + + auto is_empty = std::atomic_flag(); + scope.spawn(poller.on_empty() | + then([&]() noexcept { is_empty.test_and_set(); })); + + while (poller.wait_for(to_millis(timers.resolve())) || !is_empty.test()); +} + } // namespace net::service #endif // CPPNET_ASYNC_CONTEXT_IMPL_HPP diff --git a/include/net/service/impl/context_thread_impl.hpp b/include/net/service/impl/context_thread_impl.hpp index 6e6c31f..1b80276 100644 --- a/include/net/service/impl/context_thread_impl.hpp +++ b/include/net/service/impl/context_thread_impl.hpp @@ -25,31 +25,6 @@ #include namespace net::service { - -template -template - requires std::is_invocable_r_v -auto context_thread::isr(async_scope &scope, - const socket_dialog &socket, - Fn handle) -> void -{ - using namespace io::socket; - using namespace stdexec; - - static constexpr auto BUFSIZE = 1024UL; - static auto buffer = std::array{}; - static auto msg = socket_message{.buffers = buffer}; - - if (!handle()) - return; - - auto recvmsg = - io::recvmsg(socket, msg, 0) | - then([=, &scope](auto len) noexcept { isr(scope, socket, handle); }) | - upon_error([](auto &&err) noexcept {}); - scope.spawn(std::move(recvmsg)); -} - template auto context_thread::stop() noexcept -> void { @@ -79,7 +54,7 @@ auto context_thread::start(Args &&...args) -> void { const auto token = scope.get_stop_token(); - isr(scope, poller.emplace(sockets[0]), [&]() noexcept { + isr(poller.emplace(sockets[0]), [&]() noexcept { auto sigmask_ = sigmask.exchange(0); for (int signum = 0; auto mask = (sigmask_ >> signum); ++signum) { @@ -109,7 +84,7 @@ auto context_thread::start(Args &&...args) -> void } state.notify_all(); - run(service, token); + run(); } stop(); @@ -127,29 +102,5 @@ template context_thread::~context_thread() signal(terminate); server_.join(); } - -template -template -auto context_thread::run(Service &service, - const StopToken &token) -> void -{ - using namespace stdexec; - using namespace std::chrono; - - auto next = timers.resolve(); - int wait_ms = - (next.count() < 0) ? next.count() : duration_cast(next).count(); - - auto is_empty = std::atomic_flag(); - scope.spawn(poller.on_empty() | - then([&]() noexcept { is_empty.test_and_set(); })); - - while (poller.wait_for(wait_ms) || !is_empty.test()) - { - next = timers.resolve(); - wait_ms = (next.count() < 0) ? next.count() - : duration_cast(next).count(); - } -} } // namespace net::service #endif // CPPNET_CONTEXT_THREAD_IMPL_HPP