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