From 83e31acf2b8320049c4e0f813911f908ffe379e5 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Fri, 13 Jun 2025 18:10:58 +0300 Subject: [PATCH 01/24] Introduce signal abstractions for Unix-based event handling. --- CMakeLists.txt | 2 ++ include/coro/detail/io_notifier_epoll.hpp | 3 ++ include/coro/detail/signal_unix.hpp | 24 +++++++++++++++ include/coro/io_scheduler.hpp | 16 +++++----- include/coro/signal.hpp | 7 +++++ src/detail/io_notifier_epoll.cpp | 4 +++ src/detail/signal_unix.cpp | 34 +++++++++++++++++++++ src/io_scheduler.cpp | 36 +++-------------------- 8 files changed, 86 insertions(+), 40 deletions(-) create mode 100644 include/coro/detail/signal_unix.hpp create mode 100644 include/coro/signal.hpp create mode 100644 src/detail/signal_unix.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a27a052b..19180510 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,6 +116,8 @@ if(LIBCORO_FEATURE_NETWORKING) list(APPEND LIBCORO_SOURCE_FILES include/coro/detail/poll_info.hpp include/coro/detail/timer_handle.hpp src/detail/timer_handle.cpp + include/coro/signal.hpp + include/coro/detail/signal_unix.hpp src/detail/signal_unix.cpp include/coro/fd.hpp include/coro/io_scheduler.hpp src/io_scheduler.cpp diff --git a/include/coro/detail/io_notifier_epoll.hpp b/include/coro/detail/io_notifier_epoll.hpp index 8091b641..ae9f680b 100644 --- a/include/coro/detail/io_notifier_epoll.hpp +++ b/include/coro/detail/io_notifier_epoll.hpp @@ -13,6 +13,7 @@ #include "coro/detail/poll_info.hpp" #include "coro/fd.hpp" #include "coro/poll.hpp" +#include "coro/signal.hpp" namespace coro::detail { @@ -42,6 +43,8 @@ class io_notifier_epoll auto watch(fd_t fd, coro::poll_op op, void* data, bool keep = false) -> bool; + auto watch(const signal& signal, void* data) -> bool; + auto watch(detail::poll_info& pi) -> bool; auto unwatch(detail::poll_info& pi) -> bool; diff --git a/include/coro/detail/signal_unix.hpp b/include/coro/detail/signal_unix.hpp new file mode 100644 index 00000000..7e0d5a83 --- /dev/null +++ b/include/coro/detail/signal_unix.hpp @@ -0,0 +1,24 @@ +#pragma once +#include "coro/fd.hpp" + +#include + +namespace coro::detail +{ +class signal_unix +{ +public: + signal_unix(); + ~signal_unix(); + + void set(); + + void unset(); + + [[nodiscard]] fd_t read_fd() const noexcept { return m_pipe[0]; } + [[nodiscard]] fd_t write_fd() const noexcept { return m_pipe[1]; } + +private: + std::array m_pipe{-1}; +}; +} // namespace coro::detail \ No newline at end of file diff --git a/include/coro/io_scheduler.hpp b/include/coro/io_scheduler.hpp index 66a1ad88..50622dee 100644 --- a/include/coro/io_scheduler.hpp +++ b/include/coro/io_scheduler.hpp @@ -13,6 +13,8 @@ #include "coro/net/socket.hpp" #endif +#include "signal.hpp" + #include #include #include @@ -155,8 +157,7 @@ class io_scheduler : public std::enable_shared_from_this if (m_scheduler.m_schedule_fd_triggered.compare_exchange_strong( expected, true, std::memory_order::release, std::memory_order::relaxed)) { - const int control = 1; - ::write(m_scheduler.m_schedule_fd[1], reinterpret_cast(&control), sizeof(control)); + m_scheduler.m_schedule_signal.set(); } } else @@ -373,8 +374,7 @@ class io_scheduler : public std::enable_shared_from_this if (m_schedule_fd_triggered.compare_exchange_strong( expected, true, std::memory_order::release, std::memory_order::relaxed)) { - const int value = 1; - ::write(m_schedule_fd[1], reinterpret_cast(&value), sizeof(value)); + m_schedule_signal.set(); } return true; @@ -419,10 +419,10 @@ class io_scheduler : public std::enable_shared_from_this io_notifier m_io_notifier; /// The timer handle for timed events, e.g. yield_for() or scheduler_after(). detail::timer_handle m_timer; - /// The event loop fd to trigger a shutdown. - std::array m_shutdown_fd{-1}; - /// The schedule file descriptor if the scheduler is in inline processing mode. - std::array m_schedule_fd{-1}; + /// The event loop signal to trigger a shutdown. + signal m_shutdown_signal; + /// The schedule signal if the scheduler is in inline processing mode. + signal m_schedule_signal; std::atomic m_schedule_fd_triggered{false}; /// The number of tasks executing or awaiting events in this io scheduler. diff --git a/include/coro/signal.hpp b/include/coro/signal.hpp new file mode 100644 index 00000000..8bbcab45 --- /dev/null +++ b/include/coro/signal.hpp @@ -0,0 +1,7 @@ +#pragma once +#include "detail/signal_unix.hpp" + +namespace coro +{ +using signal = detail::signal_unix; +} // namespace coro \ No newline at end of file diff --git a/src/detail/io_notifier_epoll.cpp b/src/detail/io_notifier_epoll.cpp index 006afff9..3f45eed6 100644 --- a/src/detail/io_notifier_epoll.cpp +++ b/src/detail/io_notifier_epoll.cpp @@ -61,6 +61,10 @@ auto io_notifier_epoll::watch(fd_t fd, coro::poll_op op, void* data, bool keep) } return ::epoll_ctl(m_fd, EPOLL_CTL_ADD, fd, &event_data) != -1; } +auto io_notifier_epoll::watch(const signal& signal, void* data) -> bool +{ + return watch(signal.read_fd(), coro::poll_op::read, data, true); +} auto io_notifier_epoll::watch(detail::poll_info& pi) -> bool { diff --git a/src/detail/signal_unix.cpp b/src/detail/signal_unix.cpp new file mode 100644 index 00000000..0d1a7b5c --- /dev/null +++ b/src/detail/signal_unix.cpp @@ -0,0 +1,34 @@ +// +// Created by pyxiion on 13.06.2025. +// +#include "coro/detail/signal_unix.hpp" +#include + +namespace coro::detail +{ +signal_unix::signal_unix() +{ + ::pipe(m_pipe.data()); +} +signal_unix::~signal_unix() +{ + for (auto& fd : m_pipe) + { + if (fd != -1) + { + close(fd); + fd = -1; + } + } +} +void signal_unix::set() +{ + const int value{1}; + ::write(m_pipe[1], reinterpret_cast(&value), sizeof(value)); +} +void signal_unix::unset() +{ + int control = 0; + ::read(m_pipe[1], reinterpret_cast(&control), sizeof(control)); +} +} // namespace coro::detail \ No newline at end of file diff --git a/src/io_scheduler.cpp b/src/io_scheduler.cpp index 99f70b71..5f12449f 100644 --- a/src/io_scheduler.cpp +++ b/src/io_scheduler.cpp @@ -23,13 +23,9 @@ io_scheduler::io_scheduler(options&& opts, private_constructor) m_thread_pool = thread_pool::make_shared(std::move(m_opts.pool)); } - m_shutdown_fd = std::array{}; - ::pipe(m_shutdown_fd.data()); - m_io_notifier.watch(m_shutdown_fd[0], coro::poll_op::read, const_cast(m_shutdown_ptr), true); + m_io_notifier.watch(m_shutdown_signal, const_cast(m_shutdown_ptr)); - m_schedule_fd = std::array{}; - ::pipe(m_schedule_fd.data()); - m_io_notifier.watch(m_schedule_fd[0], coro::poll_op::read, const_cast(m_schedule_ptr), true); + m_io_notifier.watch(m_schedule_signal, const_cast(m_schedule_ptr)); m_recent_events.reserve(m_max_events); } @@ -57,28 +53,6 @@ io_scheduler::~io_scheduler() { m_io_thread.join(); } - - if (m_shutdown_fd[0] != -1) - { - close(m_shutdown_fd[0]); - m_shutdown_fd[0] = -1; - } - if (m_shutdown_fd[1] != -1) - { - close(m_shutdown_fd[1]); - m_shutdown_fd[1] = -1; - } - - if (m_schedule_fd[0] != -1) - { - close(m_schedule_fd[0]); - m_schedule_fd[0] = -1; - } - if (m_schedule_fd[1] != -1) - { - close(m_schedule_fd[1]); - m_schedule_fd[1] = -1; - } } auto io_scheduler::process_events(std::chrono::milliseconds timeout) -> std::size_t @@ -167,8 +141,7 @@ auto io_scheduler::shutdown() noexcept -> void } // Signal the event loop to stop asap, triggering the event fd is safe. - const int value{1}; - ::write(m_shutdown_fd[1], reinterpret_cast(&value), sizeof(value)); + m_shutdown_signal.set(); if (m_io_thread.joinable()) { @@ -296,8 +269,7 @@ auto io_scheduler::process_scheduled_execute_inline() -> void tasks.swap(m_scheduled_tasks); // Clear the schedule eventfd if this is a scheduled task. - int control = 0; - ::read(m_schedule_fd[1], reinterpret_cast(&control), sizeof(control)); + m_schedule_signal.unset(); // Clear the in memory flag to reduce eventfd_* calls on scheduling. m_schedule_fd_triggered.exchange(false, std::memory_order::release); From cd877c66e67daaf4708f3d8cf2e6a4e93d2f8e9e Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sat, 14 Jun 2025 00:10:57 +0300 Subject: [PATCH 02/24] IOCP, signals, sockets --- .gitignore | 2 + CMakeLists.txt | 25 ++++-- include/coro/detail/io_notifier_iocp.hpp | 46 +++++++++++ include/coro/detail/signal_win32.hpp | 26 ++++++ include/coro/io_notifier.hpp | 4 + include/coro/net/socket.hpp | 28 +++++-- include/coro/poll.hpp | 14 ++++ include/coro/signal.hpp | 12 ++- src/detail/io_notifier_iocp.cpp | 100 +++++++++++++++++++++++ src/detail/signal_win32.cpp | 33 ++++++++ src/net/socket.cpp | 85 ++++++++++++++----- 11 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 include/coro/detail/io_notifier_iocp.hpp create mode 100644 include/coro/detail/signal_win32.hpp create mode 100644 src/detail/io_notifier_iocp.cpp create mode 100644 src/detail/signal_win32.cpp diff --git a/.gitignore b/.gitignore index c8a35496..f83187d9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,7 @@ /RelWithDebInfo/ /Release/ /Testing/ +/out/ /.vscode/ +/.vs/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 19180510..6a8da3c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,7 @@ if (LIBCORO_RUN_GITCONFIG) ) endif() -cmake_dependent_option(LIBCORO_FEATURE_NETWORKING "Include networking features, Default=ON." ON "NOT EMSCRIPTEN; NOT MSVC" OFF) +cmake_dependent_option(LIBCORO_FEATURE_NETWORKING "Include networking features, Default=ON." ON "NOT EMSCRIPTEN" OFF) cmake_dependent_option(LIBCORO_FEATURE_TLS "Include TLS encryption features, Default=ON." ON "NOT EMSCRIPTEN; NOT MSVC" OFF) message("${PROJECT_NAME} LIBCORO_ENABLE_ASAN = ${LIBCORO_ENABLE_ASAN}") @@ -117,7 +117,6 @@ if(LIBCORO_FEATURE_NETWORKING) include/coro/detail/poll_info.hpp include/coro/detail/timer_handle.hpp src/detail/timer_handle.cpp include/coro/signal.hpp - include/coro/detail/signal_unix.hpp src/detail/signal_unix.cpp include/coro/fd.hpp include/coro/io_scheduler.hpp src/io_scheduler.cpp @@ -128,26 +127,34 @@ if(LIBCORO_FEATURE_NETWORKING) if(LINUX) list(APPEND LIBCORO_SOURCE_FILES include/coro/detail/io_notifier_epoll.hpp src/detail/io_notifier_epoll.cpp + include/coro/detail/signal_unix.hpp src/detail/signal_unix.cpp ) endif() if(MACOSX) list(APPEND LIBCORO_SOURCE_FILES include/coro/detail/io_notifier_kqueue.hpp src/detail/io_notifier_kqueue.cpp + include/coro/detail/signal_unix.hpp src/detail/signal_unix.cpp + ) + endif() + if(WIN32) + list(APPEND LIBCORO_SOURCE_FILES + include/coro/detail/io_notifier_iocp.hpp src/detail/io_notifier_iocp.cpp + include/coro/detail/signal_win32.hpp src/detail/signal_win32.cpp ) endif() list(APPEND LIBCORO_SOURCE_FILES - include/coro/net/dns/resolver.hpp src/net/dns/resolver.cpp - include/coro/net/connect.hpp src/net/connect.cpp - include/coro/net/hostname.hpp - include/coro/net/ip_address.hpp src/net/ip_address.cpp + #include/coro/net/dns/resolver.hpp src/net/dns/resolver.cpp + #include/coro/net/connect.hpp src/net/connect.cpp + #include/coro/net/hostname.hpp + #include/coro/net/ip_address.hpp src/net/ip_address.cpp include/coro/net/recv_status.hpp src/net/recv_status.cpp include/coro/net/send_status.hpp src/net/send_status.cpp include/coro/net/socket.hpp src/net/socket.cpp - include/coro/net/tcp/client.hpp src/net/tcp/client.cpp - include/coro/net/tcp/server.hpp src/net/tcp/server.cpp - include/coro/net/udp/peer.hpp src/net/udp/peer.cpp + #include/coro/net/tcp/client.hpp src/net/tcp/client.cpp + #include/coro/net/tcp/server.hpp src/net/tcp/server.cpp + #include/coro/net/udp/peer.hpp src/net/udp/peer.cpp ) if(LIBCORO_FEATURE_TLS) diff --git a/include/coro/detail/io_notifier_iocp.hpp b/include/coro/detail/io_notifier_iocp.hpp new file mode 100644 index 00000000..5f08a4c4 --- /dev/null +++ b/include/coro/detail/io_notifier_iocp.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include "coro/detail/poll_info.hpp" +#include "coro/fd.hpp" +#include "coro/poll.hpp" +#include "coro/signal.hpp" + +namespace coro::detail +{ +class io_notifier_iocp +{ +public: + io_notifier_iocp(); + + io_notifier_iocp(const io_notifier_iocp&) = delete; + io_notifier_iocp(io_notifier_iocp&&) = delete; + auto operator=(const io_notifier_iocp&) -> io_notifier_iocp& = delete; + auto operator=(io_notifier_iocp&&) -> io_notifier_iocp& = delete; + + ~io_notifier_iocp(); + + //auto watch_timer(const detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool; + + auto watch(const coro::signal& signal, void* data) -> bool; + + auto watch(detail::poll_info& pi) -> bool; + + auto unwatch(detail::poll_info& pi) -> bool; + + //auto unwatch_timer(const detail::timer_handle& timer) -> bool; + + auto next_events( + std::vector>& ready_events, std::chrono::milliseconds timeout) + -> void; + + //static auto event_to_poll_status(const event_t& event) -> poll_status; + +private: + void* m_iocp{}; + + void set_signal_active(void* data, bool active); + + std::mutex m_active_signals_mutex; + std::vector m_active_signals; +}; +} \ No newline at end of file diff --git a/include/coro/detail/signal_win32.hpp b/include/coro/detail/signal_win32.hpp new file mode 100644 index 00000000..4342f1c1 --- /dev/null +++ b/include/coro/detail/signal_win32.hpp @@ -0,0 +1,26 @@ +#pragma once +#include + +namespace coro::detail +{ +class signal_win32 +{ + struct Event; + friend class io_notifier_iocp; + +public: + signal_win32(); + ~signal_win32(); + + void set(); + void unset(); + + +private: + mutable void* m_iocp{}; + mutable void* m_data{}; + std::unique_ptr m_event; + + static constexpr const int signal_key = 1; +}; +} // namespace coro::detail \ No newline at end of file diff --git a/include/coro/io_notifier.hpp b/include/coro/io_notifier.hpp index 4938ad03..27e5bda8 100644 --- a/include/coro/io_notifier.hpp +++ b/include/coro/io_notifier.hpp @@ -4,6 +4,8 @@ #include "coro/detail/io_notifier_kqueue.hpp" #elif defined(__linux__) #include "coro/detail/io_notifier_epoll.hpp" +#elif defined(_WIN32) || defined(_WIN64) + #include "coro/detail/io_notifier_iocp.hpp" #endif namespace coro @@ -13,6 +15,8 @@ namespace coro using io_notifier = detail::io_notifier_kqueue; #elif defined(__linux__) using io_notifier = detail::io_notifier_epoll; +#elif defined(_WIN32) || defined(_WIN64) + using io_notifier = detail::io_notifier_iocp; #endif } // namespace coro diff --git a/include/coro/net/socket.hpp b/include/coro/net/socket.hpp index 19380c1c..6cf6b322 100644 --- a/include/coro/net/socket.hpp +++ b/include/coro/net/socket.hpp @@ -3,12 +3,15 @@ #include "coro/net/ip_address.hpp" #include "coro/poll.hpp" -#include #include #include -#include #include +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) + #include + #include +#endif + #include namespace coro::net @@ -16,6 +19,14 @@ namespace coro::net class socket { public: +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) + using native_handle = int; + constexpr static native_handle invalid_handle = -1; +#elif defined(_WIN32) || defined(_WIN64) + using native_handle_t = unsigned int; + constexpr static native_handle_t invalid_handle = ~0u; +#endif + enum class type_t { /// udp datagram socket @@ -47,9 +58,16 @@ class socket socket() = default; explicit socket(int fd) : m_fd(fd) {} + +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) socket(const socket& other) : m_fd(dup(other.m_fd)) {} - socket(socket&& other) : m_fd(std::exchange(other.m_fd, -1)) {} auto operator=(const socket& other) noexcept -> socket&; +#elif defined(_WIN32) || defined(_WIN64) + socket(const socket& other) = delete; + auto operator=(const socket& other) noexcept = delete; +#endif + + socket(socket&& other) : m_fd(std::exchange(other.m_fd, -1)) {} auto operator=(socket&& other) noexcept -> socket&; ~socket() { close(); } @@ -80,10 +98,10 @@ class socket /** * @return The native handle (file descriptor) for this socket. */ - auto native_handle() const -> int { return m_fd; } + auto native_handle() const -> native_handle_t { return m_fd; } private: - int m_fd{-1}; + native_handle_t m_fd{invalid_handle}; }; /** diff --git a/include/coro/poll.hpp b/include/coro/poll.hpp index fb846890..f67cebad 100644 --- a/include/coro/poll.hpp +++ b/include/coro/poll.hpp @@ -7,6 +7,9 @@ #if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) #include #endif +#if defined(_WIN32) || defined(_WIN64) + #include +#endif namespace coro { @@ -33,6 +36,17 @@ enum class poll_op : int64_t read_write = -5 }; #endif +#if defined(_WIN32) || defined(_WIN64) +enum class poll_op : uint32_t +{ + /// Poll for read operations. + read = FD_READ, + /// Poll for write operations. + write = FD_WRITE, + /// Poll for read and write operations. + read_write = FD_READ | FD_WRITE +}; +#endif inline auto poll_op_readable(poll_op op) -> bool { diff --git a/include/coro/signal.hpp b/include/coro/signal.hpp index 8bbcab45..55bc8786 100644 --- a/include/coro/signal.hpp +++ b/include/coro/signal.hpp @@ -1,7 +1,17 @@ #pragma once #include "detail/signal_unix.hpp" +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) + #include "detail/signal_unix.hpp" +#elif defined(_WIN32) || defined(_WIN64) + #include "detail/signal_win32.hpp" +#endif + namespace coro { -using signal = detail::signal_unix; +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) + using signal = detail::signal_unix; +#elif defined(_WIN32) || defined(_WIN64) + using signal = detail::signal_win32; +#endif } // namespace coro \ No newline at end of file diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp new file mode 100644 index 00000000..f51a2a9f --- /dev/null +++ b/src/detail/io_notifier_iocp.cpp @@ -0,0 +1,100 @@ +#include "coro/detail/io_notifier_iocp.hpp" +#include +#include "coro/detail/signal_win32.hpp" + +namespace coro::detail +{ +struct signal_win32::Event +{ + OVERLAPPED overlapped; + void* data; + bool is_set; +}; + + +io_notifier_iocp::io_notifier_iocp() +{ + m_iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); +} + +io_notifier_iocp::~io_notifier_iocp() +{ + CloseHandle(m_iocp); +} + +auto io_notifier_iocp::watch(const coro::signal& signal, void* data) -> bool +{ + signal.m_iocp = m_iocp; + signal.m_data = data; +} + +auto io_notifier_iocp::next_events( + std::vector>& ready_events, + std::chrono::milliseconds timeout +) -> void +{ + DWORD bytesTransferred = 0; + ULONG_PTR completionKey = 0; + LPOVERLAPPED overlapped = nullptr; + DWORD timeoutMs = (timeout.count() == -1) ? INFINITE : static_cast(timeout.count()); + + while (true) + { + BOOL success = GetQueuedCompletionStatus(m_iocp, &bytesTransferred, &completionKey, &overlapped, timeoutMs); + + if (!success && overlapped == nullptr) + { + // Timeout or critical error + return; + } + + if (completionKey == signal_win32::signal_key) + { + if (!overlapped) + continue; + + auto* event = reinterpret_cast(overlapped); + set_signal_active(event->data, event->is_set); + continue; + } + + if (completionKey == 2) // socket + { + auto* info = reinterpret_cast(completionKey); + if (!info) + continue; + coro::poll_status status; + + if (!success) { + DWORD err = GetLastError(); + if (err == ERROR_NETNAME_DELETED || err == ERROR_CONNECTION_ABORTED || err == ERROR_OPERATION_ABORTED) + status = coro::poll_status::closed; + else + status = coro::poll_status::error; + } + else if (bytesTransferred == 0) { + // The connection is closed normally + status = coro::poll_status::closed; + } + else { + status = coro::poll_status::event; + } + + ready_events.emplace_back(info, status); + continue; + } + + throw std::runtime_error("Received unknown completion key."); + } +} +void io_notifier_iocp::set_signal_active(void* data, bool active) +{ + std::scoped_lock lk{m_active_signals_mutex}; + if (active) { + m_active_signals.emplace_back(data); + } + else { + m_active_signals.erase(std::remove(std::begin(m_active_signals), std::end(m_active_signals), data)); + } +} +} \ No newline at end of file diff --git a/src/detail/signal_win32.cpp b/src/detail/signal_win32.cpp new file mode 100644 index 00000000..f485a023 --- /dev/null +++ b/src/detail/signal_win32.cpp @@ -0,0 +1,33 @@ +#include +#include + +namespace coro::detail +{ +// Maybe not thread-safe +struct signal_win32::Event +{ + OVERLAPPED overlapped; + void* data; + bool is_set; +}; + +signal_win32::signal_win32() : m_event(std::make_unique()) +{ + +} +signal_win32::~signal_win32() +{ +} +void signal_win32::set() +{ + m_event->is_set = true; + m_event->data = m_data; + PostQueuedCompletionStatus(m_iocp, 0, (ULONG_PTR)signal_key, (LPOVERLAPPED)(void*)m_event.get()); +} +void signal_win32::unset() +{ + m_event->is_set = false; + m_event->data = m_data; + PostQueuedCompletionStatus(m_iocp, 0, (ULONG_PTR)signal_key, (LPOVERLAPPED)(void*)m_event.get()); +} +} \ No newline at end of file diff --git a/src/net/socket.cpp b/src/net/socket.cpp index 09d36680..9e964743 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -1,8 +1,14 @@ #include "coro/net/socket.hpp" -#include +#if defined(_WIN32) || defined(_WIN64) + #include + #include +#elif defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) + #include +#endif namespace coro::net { + auto socket::type_to_os(type_t type) -> int { switch (type) @@ -16,12 +22,15 @@ auto socket::type_to_os(type_t type) -> int } } + +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) auto socket::operator=(const socket& other) noexcept -> socket& { this->close(); this->m_fd = dup(other.m_fd); return *this; } +#endif auto socket::operator=(socket&& other) noexcept -> socket& { @@ -40,6 +49,7 @@ auto socket::blocking(blocking_t block) -> bool return false; } +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) int flags = fcntl(m_fd, F_GETFL, 0); if (flags == -1) { @@ -50,44 +60,70 @@ auto socket::blocking(blocking_t block) -> bool flags = (block == blocking_t::yes) ? flags & ~O_NONBLOCK : (flags | O_NONBLOCK); return (fcntl(m_fd, F_SETFL, flags) == 0); +#elif defined(_WIN32) || defined(_WIN64) + u_long mode = (block == blocking_t::yes) ? 0 : 1; + return ioctlsocket(m_fd, FIONBIO, &mode) == 0; +#endif } auto socket::shutdown(poll_op how) -> bool { - if (m_fd != -1) + if (m_fd == -1) { - int h{0}; - switch (how) - { - case poll_op::read: - h = SHUT_RD; - break; - case poll_op::write: - h = SHUT_WR; - break; - case poll_op::read_write: - h = SHUT_RDWR; - break; - } + return false; + } - return (::shutdown(m_fd, h) == 0); + int h = 0; +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) + // POSIX systems use SHUT_RD, SHUT_WR, SHUT_RDWR + switch (how) + { + case poll_op::read: + h = SHUT_RD; + break; + case poll_op::write: + h = SHUT_WR; + break; + case poll_op::read_write: + h = SHUT_RDWR; + break; } - return false; +#elif defined(_WIN32) || defined(_WIN64) + // WinSock uses SD_RECEIVE, SD_SEND, SD_BOTH + switch (how) + { + case poll_op::read: + h = SD_RECEIVE; + break; + case poll_op::write: + h = SD_SEND; + break; + case poll_op::read_write: + h = SD_BOTH; + break; + } +#endif + return (::shutdown((SOCKET)m_fd, h) == 0); } auto socket::close() -> void { if (m_fd != -1) { + +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) ::close(m_fd); - m_fd = -1; +#elif defined(_WIN32) || defined(_WIN64) + ::closesocket(m_fd); +#endif + m_fd = socket::invalid_handle; } } auto make_socket(const socket::options& opts) -> socket { socket s{::socket(static_cast(opts.domain), socket::type_to_os(opts.type), 0)}; - if (s.native_handle() < 0) + if (s.native_handle() != socket::invalid_handle) { throw std::runtime_error{"Failed to create socket."}; } @@ -112,12 +148,17 @@ auto make_accept_socket(const socket::options& opts, const net::ip_address& addr // BSD and macOS use a different SO_REUSEPORT implementation than Linux that enables both duplicate address and port // bindings with a single flag. #if defined(__linux__) - int sock_opt_name = SO_REUSEADDR | SO_REUSEPORT; + int sock_opt_name = SO_REUSEADDR | SO_REUSEPORT; + int& sock_opt_ptr = &sock_opt; #elif defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) - int sock_opt_name = SO_REUSEPORT; + int sock_opt_name = SO_REUSEPORT; + int& sock_opt_ptr = &sock_opt; +#elif defined(_WIN32) || defined(_WIN64) + int sock_opt_name = SO_REUSEADDR; + const char *sock_opt_ptr = reinterpret_cast(&sock_opt); #endif - if (setsockopt(s.native_handle(), SOL_SOCKET, sock_opt_name, &sock_opt, sizeof(sock_opt)) < 0) + if (setsockopt(s.native_handle(), SOL_SOCKET, sock_opt_name, sock_opt_ptr, sizeof(sock_opt)) < 0) { throw std::runtime_error{"Failed to setsockopt(SO_REUSEADDR | SO_REUSEPORT)"}; } From 28c48826db437557bdf7dd516ac6a8a533554e94 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Thu, 19 Jun 2025 15:07:10 +0300 Subject: [PATCH 03/24] Platform macros --- include/coro/io_notifier.hpp | 13 +++++++------ include/coro/io_scheduler.hpp | 6 +++++- include/coro/net/dns/resolver.hpp | 5 +++++ include/coro/net/ip_address.hpp | 8 +++++++- include/coro/net/socket.hpp | 11 ++++++----- include/coro/net/tcp/server.hpp | 5 ++++- include/coro/platform.hpp | 11 +++++++++++ include/coro/poll.hpp | 15 +++++++-------- include/coro/signal.hpp | 10 +++++----- src/io_scheduler.cpp | 8 ++++++-- 10 files changed, 63 insertions(+), 29 deletions(-) create mode 100644 include/coro/platform.hpp diff --git a/include/coro/io_notifier.hpp b/include/coro/io_notifier.hpp index 27e5bda8..2888794d 100644 --- a/include/coro/io_notifier.hpp +++ b/include/coro/io_notifier.hpp @@ -1,21 +1,22 @@ #pragma once +#include "platform.hpp" -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) +#if defined(CORO_PLATFORM_BSD) #include "coro/detail/io_notifier_kqueue.hpp" -#elif defined(__linux__) +#elif defined(CORO_PLATFORM_LINUX) #include "coro/detail/io_notifier_epoll.hpp" -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) #include "coro/detail/io_notifier_iocp.hpp" #endif namespace coro { -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) +#if defined(CORO_PLATFORM_BSD) using io_notifier = detail::io_notifier_kqueue; -#elif defined(__linux__) +#elif defined(CORO_PLATFORM_LINUX) using io_notifier = detail::io_notifier_epoll; -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) using io_notifier = detail::io_notifier_iocp; #endif diff --git a/include/coro/io_scheduler.hpp b/include/coro/io_scheduler.hpp index 50622dee..db3f2862 100644 --- a/include/coro/io_scheduler.hpp +++ b/include/coro/io_scheduler.hpp @@ -7,7 +7,11 @@ #include "coro/io_notifier.hpp" #include "coro/poll.hpp" #include "coro/thread_pool.hpp" -#include +#include "coro/platform.hpp" + +#ifdef CORO_PLATFORM_UNIX + #include +#endif #ifdef LIBCORO_FEATURE_NETWORKING #include "coro/net/socket.hpp" diff --git a/include/coro/net/dns/resolver.hpp b/include/coro/net/dns/resolver.hpp index 84592720..dd00ab88 100644 --- a/include/coro/net/dns/resolver.hpp +++ b/include/coro/net/dns/resolver.hpp @@ -7,10 +7,15 @@ #include "coro/net/ip_address.hpp" #include "coro/poll.hpp" #include "coro/task.hpp" +#include "coro/platform.hpp" #include + +#if defined(CORO_PLATFORM_UNIX) #include #include +#elif defined(CORO_PLATFORM_WINDOWS) +#endif #include #include diff --git a/include/coro/net/ip_address.hpp b/include/coro/net/ip_address.hpp index 34512c0e..006769c0 100644 --- a/include/coro/net/ip_address.hpp +++ b/include/coro/net/ip_address.hpp @@ -1,12 +1,18 @@ #pragma once #include -#include #include #include #include #include #include +#include "coro/platform.hpp" + +#if defined(CORO_PLATFORM_UNIX) + #include +#elif defined(CORO_PLATFORM_WINDOWS) + #include +#endif namespace coro::net { diff --git a/include/coro/net/socket.hpp b/include/coro/net/socket.hpp index 6cf6b322..9b7cbe95 100644 --- a/include/coro/net/socket.hpp +++ b/include/coro/net/socket.hpp @@ -2,12 +2,13 @@ #include "coro/net/ip_address.hpp" #include "coro/poll.hpp" +#include "coro/platform.hpp" #include #include #include -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) #include #include #endif @@ -19,10 +20,10 @@ namespace coro::net class socket { public: -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) using native_handle = int; constexpr static native_handle invalid_handle = -1; -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) using native_handle_t = unsigned int; constexpr static native_handle_t invalid_handle = ~0u; #endif @@ -59,10 +60,10 @@ class socket explicit socket(int fd) : m_fd(fd) {} -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) socket(const socket& other) : m_fd(dup(other.m_fd)) {} auto operator=(const socket& other) noexcept -> socket&; -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) socket(const socket& other) = delete; auto operator=(const socket& other) noexcept = delete; #endif diff --git a/include/coro/net/tcp/server.hpp b/include/coro/net/tcp/server.hpp index e038067e..1ada9912 100644 --- a/include/coro/net/tcp/server.hpp +++ b/include/coro/net/tcp/server.hpp @@ -4,9 +4,12 @@ #include "coro/net/socket.hpp" #include "coro/net/tcp/client.hpp" #include "coro/task.hpp" +#include "coro/platform.hpp" #include -#include +#if defined(CORO_PLATFORM_UNIX) + #include +#endif namespace coro { diff --git a/include/coro/platform.hpp b/include/coro/platform.hpp new file mode 100644 index 00000000..8f14a6cf --- /dev/null +++ b/include/coro/platform.hpp @@ -0,0 +1,11 @@ +#pragma once + +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) + #define CORO_PLATFORM_UNIX + #define CORO_PLATFORM_BSD +#elif defined(__linux__) + #define CORO_PLATFORM_UNIX + #define CORO_PLATFORM_LINUX +#elif defined(_WIN32) || defined(_WIN64) + #define CORO_PLATFORM_WINDOWS +#endif \ No newline at end of file diff --git a/include/coro/poll.hpp b/include/coro/poll.hpp index f67cebad..4764be7c 100644 --- a/include/coro/poll.hpp +++ b/include/coro/poll.hpp @@ -1,19 +1,20 @@ #pragma once #include -#if defined(__linux__) +#include "platform.hpp" +#if defined(CORO_PLATFORM_LINUX) #include #endif -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) +#if defined(CORO_PLATFORM_BSD) #include #endif -#if defined(_WIN32) || defined(_WIN64) +#if defined(CORO_PLATFORM_WINDOWS) #include #endif namespace coro { -#if defined(__linux__) +#if defined(CORO_PLATFORM_LINUX) enum class poll_op : uint64_t { /// Poll for read operations. @@ -23,8 +24,7 @@ enum class poll_op : uint64_t /// Poll for read and write operations. read_write = EPOLLIN | EPOLLOUT }; -#endif -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) +#elif defined(CORO_PLATFORM_BSD) enum class poll_op : int64_t { /// Poll for read operations. @@ -35,8 +35,7 @@ enum class poll_op : int64_t // read_write = EVFILT_READ | EVFILT_WRITE read_write = -5 }; -#endif -#if defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) enum class poll_op : uint32_t { /// Poll for read operations. diff --git a/include/coro/signal.hpp b/include/coro/signal.hpp index 55bc8786..43ca7e4f 100644 --- a/include/coro/signal.hpp +++ b/include/coro/signal.hpp @@ -1,17 +1,17 @@ #pragma once -#include "detail/signal_unix.hpp" +#include "coro/platform.hpp" -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) #include "detail/signal_unix.hpp" -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) #include "detail/signal_win32.hpp" #endif namespace coro { -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) using signal = detail::signal_unix; -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) using signal = detail::signal_win32; #endif } // namespace coro \ No newline at end of file diff --git a/src/io_scheduler.cpp b/src/io_scheduler.cpp index 5f12449f..786449e9 100644 --- a/src/io_scheduler.cpp +++ b/src/io_scheduler.cpp @@ -1,12 +1,16 @@ #include "coro/io_scheduler.hpp" #include "coro/detail/task_self_deleting.hpp" +#include "coro/platform.hpp" #include #include #include -#include #include -#include + +#if defined(CORO_PLATFORM_UNIX) + #include + #include +#endif using namespace std::chrono_literals; From faa03fa7b927504ba348ec7bc675adee9c73f908 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Fri, 20 Jun 2025 08:14:27 +0300 Subject: [PATCH 04/24] Adds read/write methods and noexcept to move constructor. --- include/coro/net/tcp/client.hpp | 77 ++++++++++++++++++++++++++++++++- src/net/tcp/client.cpp | 2 +- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/include/coro/net/tcp/client.hpp b/include/coro/net/tcp/client.hpp index d53cd700..e7ebc9f4 100644 --- a/include/coro/net/tcp/client.hpp +++ b/include/coro/net/tcp/client.hpp @@ -7,6 +7,7 @@ #include "coro/net/recv_status.hpp" #include "coro/net/send_status.hpp" #include "coro/net/socket.hpp" +#include "coro/platform.hpp" #include "coro/poll.hpp" #include "coro/task.hpp" @@ -43,7 +44,7 @@ class client .port = 8080, }); client(const client& other); - client(client&& other); + client(client&& other) noexcept; auto operator=(const client& other) noexcept -> client&; auto operator=(client&& other) noexcept -> client&; ~client(); @@ -64,9 +65,11 @@ class client */ auto connect(std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task; +#if defined(CORO_PLATFORM_UNIX) /** * Polls for the given operation on this client's tcp socket. This should be done prior to * calling recv and after a send that doesn't send the entire buffer. + * @warning Unix only * @param op The poll operation to perform, use read for incoming data and write for outgoing. * @param timeout The amount of time to wait for the poll event to be ready. Use zero for infinte timeout. * @return The status result of th poll operation. When poll_status::event is returned then the @@ -81,6 +84,7 @@ class client /** * Receives incoming data into the given buffer. By default since all tcp client sockets are set * to non-blocking use co_await poll() to determine when data is ready to be received. + * @warning Unix only * @param buffer Received bytes are written into this buffer up to the buffers size. * @return The status of the recv call and a span of the bytes recevied (if any). The span of * bytes will be a subspan or full span of the given input buffer. @@ -117,6 +121,7 @@ class client * to determine when the tcp client socket is ready to be written to again. On partial writes * the status will be 'ok' and the span returned will be non-empty, it will contain the buffer * span data that was not written to the client's socket. + * @warning Unix only * @param buffer The data to write on the tcp socket. * @return The status of the send call and a span of any remaining bytes not sent. If all bytes * were successfully sent the status will be 'ok' and the remaining span will be empty. @@ -142,6 +147,34 @@ class client return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; } } +#endif + + /** + * Attempts to send the given data to the connected peer. + * + * If only part of the data is sent, the returned span will contain the remaining bytes. + * The operation may time out if the connection is not ready. + * + * @param buffer The data to send. + * @param timeout Maximum time to wait for the operation to complete. Zero means no timeout. + * @return A pair containing the status and a span of any unsent data. If successful, the span will be empty. + */ + template + auto write(const buffer_type& buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + -> task>>; + + /** + * Attempts to receive data from the connected peer into the provided buffer. + * + * The operation may time out if no data is received within the given duration. + * + * @param buffer The buffer to fill with incoming data. + * @param timeout Maximum time to wait for the operation to complete. Zero means no timeout. + * @return A pair containing the status and a span of received bytes. The span may be empty. + */ + template + auto read(buffer_type&& buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + -> std::pair>; private: /// The tcp::server creates already connected clients and provides a tcp socket pre-built. @@ -158,4 +191,46 @@ class client std::optional m_connect_status{std::nullopt}; }; +#if defined(CORO_PLATFORM_UNIX) +template +auto client::write(const buffer_type& buffer, std::chrono::milliseconds timeout) + -> task>> +{ + if (auto status = co_await poll(poll_op::write, timeout); status != poll_status::event) + { + switch (status) + { + case poll_status::closed: + co_return {send_status::closed, std::span{buffer.data(), buffer.size()}}; + case poll_status::error: + co_return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; + case poll_status::timeout: + // TODO + break; + } + } + co_return send(buffer); +} + +template +auto client::read(buffer_type&& buffer, std::chrono::milliseconds timeout) -> std::pair> +{ + if (auto status = co_await poll(poll_op::read, timeout); status != poll_status::event) + { + switch (status) + { + case poll_status::closed: + co_return {recv_status::closed, std::span{buffer.data(), buffer.size()}}; + case poll_status::error: + co_return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; + case poll_status::timeout: + // TODO + break; + } + } + co_return recv(std::forward(buffer)); +} +#elif defined(CORO_PLATFORM_WINDOWS) +#endif + } // namespace coro::net::tcp diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index 2b322b34..ec141358 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -36,7 +36,7 @@ client::client(const client& other) { } -client::client(client&& other) +client::client(client&& other) noexcept : m_io_scheduler(std::move(other.m_io_scheduler)), m_options(std::move(other.m_options)), m_socket(std::move(other.m_socket)), From 0cc7d0c3983b00c16e90b04d90870b6e9a2da67e Mon Sep 17 00:00:00 2001 From: PyXiion Date: Fri, 20 Jun 2025 08:14:37 +0300 Subject: [PATCH 05/24] Uses trailing return type for pipe accessors --- include/coro/detail/signal_unix.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/coro/detail/signal_unix.hpp b/include/coro/detail/signal_unix.hpp index 7e0d5a83..cfa7749c 100644 --- a/include/coro/detail/signal_unix.hpp +++ b/include/coro/detail/signal_unix.hpp @@ -15,8 +15,8 @@ class signal_unix void unset(); - [[nodiscard]] fd_t read_fd() const noexcept { return m_pipe[0]; } - [[nodiscard]] fd_t write_fd() const noexcept { return m_pipe[1]; } + [[nodiscard]] auto read_fd() const noexcept -> fd_t { return m_pipe[0]; } + [[nodiscard]] auto write_fd() const noexcept -> fd_t { return m_pipe[1]; } private: std::array m_pipe{-1}; From f6589e0243c7026198dc8daf2af780fabdd9293a Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sun, 22 Jun 2025 17:34:50 +0300 Subject: [PATCH 06/24] socket code refactor --- include/coro/net/socket.hpp | 2 +- src/net/socket.cpp | 38 ++++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/include/coro/net/socket.hpp b/include/coro/net/socket.hpp index 9b7cbe95..53c85eed 100644 --- a/include/coro/net/socket.hpp +++ b/include/coro/net/socket.hpp @@ -25,7 +25,7 @@ class socket constexpr static native_handle invalid_handle = -1; #elif defined(CORO_PLATFORM_WINDOWS) using native_handle_t = unsigned int; - constexpr static native_handle_t invalid_handle = ~0u; + constexpr static native_handle_t invalid_handle = ~0u; // ~0 = -1, but for unsigned #endif enum class type_t diff --git a/src/net/socket.cpp b/src/net/socket.cpp index 9e964743..2a1a482a 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -1,8 +1,8 @@ #include "coro/net/socket.hpp" -#if defined(_WIN32) || defined(_WIN64) +#if defined(CORO_PLATFORM_WINDOWS) #include #include -#elif defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#elif defined(CORO_PLATFORM_UNIX) #include #endif @@ -23,7 +23,7 @@ auto socket::type_to_os(type_t type) -> int } -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) auto socket::operator=(const socket& other) noexcept -> socket& { this->close(); @@ -49,7 +49,7 @@ auto socket::blocking(blocking_t block) -> bool return false; } -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) int flags = fcntl(m_fd, F_GETFL, 0); if (flags == -1) { @@ -60,7 +60,7 @@ auto socket::blocking(blocking_t block) -> bool flags = (block == blocking_t::yes) ? flags & ~O_NONBLOCK : (flags | O_NONBLOCK); return (fcntl(m_fd, F_SETFL, flags) == 0); -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) u_long mode = (block == blocking_t::yes) ? 0 : 1; return ioctlsocket(m_fd, FIONBIO, &mode) == 0; #endif @@ -74,7 +74,7 @@ auto socket::shutdown(poll_op how) -> bool } int h = 0; -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) // POSIX systems use SHUT_RD, SHUT_WR, SHUT_RDWR switch (how) { @@ -88,7 +88,7 @@ auto socket::shutdown(poll_op how) -> bool h = SHUT_RDWR; break; } -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) // WinSock uses SD_RECEIVE, SD_SEND, SD_BOTH switch (how) { @@ -111,9 +111,9 @@ auto socket::close() -> void if (m_fd != -1) { -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__linux__) +#if defined(CORO_PLATFORM_UNIX) ::close(m_fd); -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) ::closesocket(m_fd); #endif m_fd = socket::invalid_handle; @@ -122,11 +122,19 @@ auto socket::close() -> void auto make_socket(const socket::options& opts) -> socket { +#if defined(CORO_PLATFORM_UNIX) socket s{::socket(static_cast(opts.domain), socket::type_to_os(opts.type), 0)}; if (s.native_handle() != socket::invalid_handle) { throw std::runtime_error{"Failed to create socket."}; } +#elif defined(CORO_PLATFORM_WINDOWS) + socket s{::WSASocketA(static_cast(opts.domain), socket::type_to_os(opts.type), 0, NULL, 0, WSA_FLAG_OVERLAPPED)}; + if (s.native_handle() != INVALID_SOCKET) + { + throw std::runtime_error{"Failed to create socket."}; + } +#endif if (opts.blocking == socket::blocking_t::no) { @@ -147,20 +155,20 @@ auto make_accept_socket(const socket::options& opts, const net::ip_address& addr int sock_opt{1}; // BSD and macOS use a different SO_REUSEPORT implementation than Linux that enables both duplicate address and port // bindings with a single flag. -#if defined(__linux__) +#if defined(CORO_PLATFORM_LINUX) int sock_opt_name = SO_REUSEADDR | SO_REUSEPORT; - int& sock_opt_ptr = &sock_opt; -#elif defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) + int* sock_opt_ptr = &sock_opt; +#elif defined(CORO_PLATFORM_BSD) int sock_opt_name = SO_REUSEPORT; - int& sock_opt_ptr = &sock_opt; -#elif defined(_WIN32) || defined(_WIN64) + int* sock_opt_ptr = &sock_opt; +#elif defined(CORO_PLATFORM_WINDOWS) int sock_opt_name = SO_REUSEADDR; const char *sock_opt_ptr = reinterpret_cast(&sock_opt); #endif if (setsockopt(s.native_handle(), SOL_SOCKET, sock_opt_name, sock_opt_ptr, sizeof(sock_opt)) < 0) { - throw std::runtime_error{"Failed to setsockopt(SO_REUSEADDR | SO_REUSEPORT)"}; + throw std::runtime_error{"Failed to setsockopt."}; } sockaddr_in server{}; From d8de051ac7ea91ea81f9e12ccbce23765dba6671 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sun, 22 Jun 2025 18:59:42 +0300 Subject: [PATCH 07/24] Fixes socket API calls and `native_handle_t` type. --- include/coro/net/socket.hpp | 4 ++-- src/net/socket.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/coro/net/socket.hpp b/include/coro/net/socket.hpp index 9b7cbe95..0f3da0a0 100644 --- a/include/coro/net/socket.hpp +++ b/include/coro/net/socket.hpp @@ -21,8 +21,8 @@ class socket { public: #if defined(CORO_PLATFORM_UNIX) - using native_handle = int; - constexpr static native_handle invalid_handle = -1; + using native_handle_t = int; + constexpr static native_handle_t invalid_handle = -1; #elif defined(CORO_PLATFORM_WINDOWS) using native_handle_t = unsigned int; constexpr static native_handle_t invalid_handle = ~0u; diff --git a/src/net/socket.cpp b/src/net/socket.cpp index 9e964743..4aecebb4 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -103,7 +103,7 @@ auto socket::shutdown(poll_op how) -> bool break; } #endif - return (::shutdown((SOCKET)m_fd, h) == 0); + return (::shutdown(m_fd, h) == 0); } auto socket::close() -> void @@ -123,7 +123,7 @@ auto socket::close() -> void auto make_socket(const socket::options& opts) -> socket { socket s{::socket(static_cast(opts.domain), socket::type_to_os(opts.type), 0)}; - if (s.native_handle() != socket::invalid_handle) + if (s.native_handle() == socket::invalid_handle) { throw std::runtime_error{"Failed to create socket."}; } @@ -149,10 +149,10 @@ auto make_accept_socket(const socket::options& opts, const net::ip_address& addr // bindings with a single flag. #if defined(__linux__) int sock_opt_name = SO_REUSEADDR | SO_REUSEPORT; - int& sock_opt_ptr = &sock_opt; + int* sock_opt_ptr = &sock_opt; #elif defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) int sock_opt_name = SO_REUSEPORT; - int& sock_opt_ptr = &sock_opt; + int* sock_opt_ptr = &sock_opt; #elif defined(_WIN32) || defined(_WIN64) int sock_opt_name = SO_REUSEADDR; const char *sock_opt_ptr = reinterpret_cast(&sock_opt); From 60f2c90a79402392bb432d6e79640ceced85d8eb Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sun, 22 Jun 2025 18:59:59 +0300 Subject: [PATCH 08/24] Uncomments network-related source files in CMakeLists. --- CMakeLists.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a8da3c5..c6f7b80f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -145,16 +145,16 @@ if(LIBCORO_FEATURE_NETWORKING) list(APPEND LIBCORO_SOURCE_FILES - #include/coro/net/dns/resolver.hpp src/net/dns/resolver.cpp - #include/coro/net/connect.hpp src/net/connect.cpp - #include/coro/net/hostname.hpp - #include/coro/net/ip_address.hpp src/net/ip_address.cpp + include/coro/net/dns/resolver.hpp src/net/dns/resolver.cpp + include/coro/net/connect.hpp src/net/connect.cpp + include/coro/net/hostname.hpp + include/coro/net/ip_address.hpp src/net/ip_address.cpp include/coro/net/recv_status.hpp src/net/recv_status.cpp include/coro/net/send_status.hpp src/net/send_status.cpp include/coro/net/socket.hpp src/net/socket.cpp - #include/coro/net/tcp/client.hpp src/net/tcp/client.cpp - #include/coro/net/tcp/server.hpp src/net/tcp/server.cpp - #include/coro/net/udp/peer.hpp src/net/udp/peer.cpp + include/coro/net/tcp/client.hpp src/net/tcp/client.cpp + include/coro/net/tcp/server.hpp src/net/tcp/server.cpp + include/coro/net/udp/peer.hpp src/net/udp/peer.cpp ) if(LIBCORO_FEATURE_TLS) From 627cc66586f39d8a6eeefe9c8b629341d1b1cdfa Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sun, 22 Jun 2025 20:27:01 +0300 Subject: [PATCH 09/24] Uncomments network-related source files in CMakeLists. --- CMakeLists.txt | 2 + include/coro/net/read_status.hpp | 12 +++ include/coro/net/tcp/client.hpp | 149 +++++++++++++++++------------- include/coro/net/write_status.hpp | 12 +++ src/net/socket.cpp | 3 +- src/net/tcp/client.cpp | 7 ++ test/net/test_tcp_server.cpp | 32 +++---- 7 files changed, 132 insertions(+), 85 deletions(-) create mode 100644 include/coro/net/read_status.hpp create mode 100644 include/coro/net/write_status.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c6f7b80f..23e82139 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,8 @@ if(LIBCORO_FEATURE_NETWORKING) include/coro/net/ip_address.hpp src/net/ip_address.cpp include/coro/net/recv_status.hpp src/net/recv_status.cpp include/coro/net/send_status.hpp src/net/send_status.cpp + include/coro/net/write_status.hpp + include/coro/net/read_status.hpp include/coro/net/socket.hpp src/net/socket.cpp include/coro/net/tcp/client.hpp src/net/tcp/client.cpp include/coro/net/tcp/server.hpp src/net/tcp/server.cpp diff --git a/include/coro/net/read_status.hpp b/include/coro/net/read_status.hpp new file mode 100644 index 00000000..7f4cccc4 --- /dev/null +++ b/include/coro/net/read_status.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace coro::net +{ +enum class read_status +{ + ok, + closed, + timeout, + error +}; +} \ No newline at end of file diff --git a/include/coro/net/tcp/client.hpp b/include/coro/net/tcp/client.hpp index e7ebc9f4..a9b4dcb4 100644 --- a/include/coro/net/tcp/client.hpp +++ b/include/coro/net/tcp/client.hpp @@ -4,9 +4,11 @@ #include "coro/io_scheduler.hpp" #include "coro/net/connect.hpp" #include "coro/net/ip_address.hpp" +#include "coro/net/read_status.hpp" #include "coro/net/recv_status.hpp" #include "coro/net/send_status.hpp" #include "coro/net/socket.hpp" +#include "coro/net/write_status.hpp" #include "coro/platform.hpp" #include "coro/poll.hpp" #include "coro/task.hpp" @@ -76,10 +78,7 @@ class client * event operation is ready. */ auto poll(coro::poll_op op, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> coro::task - { - return m_io_scheduler->poll(m_socket, op, timeout); - } + -> coro::task; /** * Receives incoming data into the given buffer. By default since all tcp client sockets are set @@ -90,31 +89,7 @@ class client * bytes will be a subspan or full span of the given input buffer. */ template - auto recv(buffer_type&& buffer) -> std::pair> - { - // If the user requested zero bytes, just return. - if (buffer.empty()) - { - return {recv_status::ok, std::span{}}; - } - - auto bytes_recv = ::recv(m_socket.native_handle(), buffer.data(), buffer.size(), 0); - if (bytes_recv > 0) - { - // Ok, we've recieved some data. - return {recv_status::ok, std::span{buffer.data(), static_cast(bytes_recv)}}; - } - else if (bytes_recv == 0) - { - // On TCP stream sockets 0 indicates the connection has been closed by the peer. - return {recv_status::closed, std::span{}}; - } - else - { - // Report the error to the user. - return {static_cast(errno), std::span{}}; - } - } + auto recv(buffer_type&& buffer) -> std::pair>; /** * Sends outgoing data from the given buffer. If a partial write occurs then use co_await poll() @@ -127,26 +102,7 @@ class client * were successfully sent the status will be 'ok' and the remaining span will be empty. */ template - auto send(const buffer_type& buffer) -> std::pair> - { - // If the user requested zero bytes, just return. - if (buffer.empty()) - { - return {send_status::ok, std::span{buffer.data(), buffer.size()}}; - } - - auto bytes_sent = ::send(m_socket.native_handle(), buffer.data(), buffer.size(), 0); - if (bytes_sent >= 0) - { - // Some or all of the bytes were written. - return {send_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; - } - else - { - // Due to the error none of the bytes were written. - return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; - } - } + auto send(const buffer_type& buffer) -> std::pair>; #endif /** @@ -161,7 +117,7 @@ class client */ template auto write(const buffer_type& buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> task>>; + -> task>>; /** * Attempts to receive data from the connected peer into the provided buffer. @@ -174,7 +130,7 @@ class client */ template auto read(buffer_type&& buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> std::pair>; + -> task>>; private: /// The tcp::server creates already connected clients and provides a tcp socket pre-built. @@ -192,43 +148,110 @@ class client }; #if defined(CORO_PLATFORM_UNIX) +template +auto client::recv(buffer_type&& buffer) -> std::pair> +{ + // If the user requested zero bytes, just return. + if (buffer.empty()) + { + return {recv_status::ok, std::span{}}; + } + + auto bytes_recv = ::recv(m_socket.native_handle(), buffer.data(), buffer.size(), 0); + if (bytes_recv > 0) + { + // Ok, we've recieved some data. + return {recv_status::ok, std::span{buffer.data(), static_cast(bytes_recv)}}; + } + else if (bytes_recv == 0) + { + // On TCP stream sockets 0 indicates the connection has been closed by the peer. + return {recv_status::closed, std::span{}}; + } + else + { + // Report the error to the user. + return {static_cast(errno), std::span{}}; + } +} + +template +auto client::send(const buffer_type& buffer) -> std::pair> +{ + // If the user requested zero bytes, just return. + if (buffer.empty()) + { + return {send_status::ok, std::span{buffer.data(), buffer.size()}}; + } + + auto bytes_sent = ::send(m_socket.native_handle(), buffer.data(), buffer.size(), 0); + if (bytes_sent >= 0) + { + // Some or all of the bytes were written. + return {send_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; + } + else + { + // Due to the error none of the bytes were written. + return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; + } +} + template auto client::write(const buffer_type& buffer, std::chrono::milliseconds timeout) - -> task>> + -> task>> { if (auto status = co_await poll(poll_op::write, timeout); status != poll_status::event) { switch (status) { case poll_status::closed: - co_return {send_status::closed, std::span{buffer.data(), buffer.size()}}; + co_return {write_status::closed, std::span{buffer.data(), buffer.size()}}; case poll_status::error: - co_return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; + co_return {write_status::error, std::span{buffer.data(), buffer.size()}}; case poll_status::timeout: - // TODO - break; + co_return {write_status::timeout, std::span{buffer.data(), buffer.size()}}; + default: + throw std::runtime_error("Unknown poll_status value."); } } - co_return send(buffer); + switch (auto &&[status, span] = send(std::forward(buffer)); status) + { + case send_status::ok: + co_return {write_status::ok, span}; + case send_status::closed: + co_return {write_status::closed, span}; + default: + co_return {write_status::error, span}; + } } template -auto client::read(buffer_type&& buffer, std::chrono::milliseconds timeout) -> std::pair> +auto client::read(buffer_type&& buffer, std::chrono::milliseconds timeout) -> task>> { if (auto status = co_await poll(poll_op::read, timeout); status != poll_status::event) { switch (status) { case poll_status::closed: - co_return {recv_status::closed, std::span{buffer.data(), buffer.size()}}; + co_return {read_status::closed, std::span{}}; case poll_status::error: - co_return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; + co_return {read_status::error, std::span{}}; case poll_status::timeout: - // TODO - break; + co_return {read_status::timeout, std::span{}}; + default: + throw std::runtime_error("Unknown poll_status value."); } } - co_return recv(std::forward(buffer)); + switch (auto&& [status, span] = recv(std::forward(buffer)); status) + { + case recv_status::ok: + co_return {read_status::ok, span}; + case recv_status::closed: + co_return {read_status::closed, span}; + default: + co_return {read_status::error, span}; + } } #elif defined(CORO_PLATFORM_WINDOWS) #endif diff --git a/include/coro/net/write_status.hpp b/include/coro/net/write_status.hpp new file mode 100644 index 00000000..cd301541 --- /dev/null +++ b/include/coro/net/write_status.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace coro::net +{ +enum class write_status +{ + ok, + closed, + timeout, + error +}; +} \ No newline at end of file diff --git a/src/net/socket.cpp b/src/net/socket.cpp index e5d3855e..be4d7035 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -88,6 +88,7 @@ auto socket::shutdown(poll_op how) -> bool h = SHUT_RDWR; break; } + return (::shutdown(m_fd, h) == 0); #elif defined(_WIN32) || defined(_WIN64) // WinSock uses SD_RECEIVE, SD_SEND, SD_BOTH switch (how) @@ -102,8 +103,8 @@ auto socket::shutdown(poll_op how) -> bool h = SD_BOTH; break; } -#endif return (::shutdown((SOCKET)m_fd, h) == 0); +#endif } auto socket::close() -> void diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index ec141358..cd70f0f5 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -129,4 +129,11 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task coro::task +{ + return m_io_scheduler->poll(m_socket, op, timeout); +} +#endif + } // namespace coro::net::tcp diff --git a/test/net/test_tcp_server.cpp b/test/net/test_tcp_server.cpp index 95686af5..5a9949c9 100644 --- a/test/net/test_tcp_server.cpp +++ b/test/net/test_tcp_server.cpp @@ -27,20 +27,15 @@ TEST_CASE("tcp_server ping server", "[tcp_server]") // Skip polling for write, should really only poll if the write is partial, shouldn't be // required for this test. - std::cerr << "client send()\n"; - auto [sstatus, remaining] = client.send(client_msg); - REQUIRE(sstatus == coro::net::send_status::ok); + std::cerr << "client write()\n"; + auto [sstatus, remaining] = co_await client.write(client_msg); + REQUIRE(sstatus == coro::net::write_status::ok); REQUIRE(remaining.empty()); - // Poll for the server's response. - std::cerr << "client poll(read)\n"; - auto pstatus = co_await client.poll(coro::poll_op::read); - REQUIRE(pstatus == coro::poll_status::event); - std::string buffer(256, '\0'); - std::cerr << "client recv()\n"; - auto [rstatus, rspan] = client.recv(buffer); - REQUIRE(rstatus == coro::net::recv_status::ok); + std::cerr << "client read()\n"; + auto [rstatus, rspan] = co_await client.read(buffer); + REQUIRE(rstatus == coro::net::read_status::ok); REQUIRE(rspan.size() == server_msg.length()); buffer.resize(rspan.size()); REQUIRE(buffer == server_msg); @@ -64,23 +59,18 @@ TEST_CASE("tcp_server ping server", "[tcp_server]") auto client = server.accept(); REQUIRE(client.socket().is_valid()); - // Poll for client request. - std::cerr << "server poll(read)\n"; - pstatus = co_await client.poll(coro::poll_op::read); - REQUIRE(pstatus == coro::poll_status::event); - std::string buffer(256, '\0'); - std::cerr << "server recv()\n"; - auto [rstatus, rspan] = client.recv(buffer); - REQUIRE(rstatus == coro::net::recv_status::ok); + std::cerr << "server read()\n"; + auto [rstatus, rspan] = co_await client.read(buffer); + REQUIRE(rstatus == coro::net::read_status::ok); REQUIRE(rspan.size() == client_msg.size()); buffer.resize(rspan.size()); REQUIRE(buffer == client_msg); // Respond to client. std::cerr << "server send()\n"; - auto [sstatus, remaining] = client.send(server_msg); - REQUIRE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await client.write(server_msg); + REQUIRE(sstatus == coro::net::write_status::ok); REQUIRE(remaining.empty()); std::cerr << "server return\n"; From ddd6f1a92ff4007f4d95e68238f1da5bfd386fa9 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sun, 22 Jun 2025 20:27:01 +0300 Subject: [PATCH 10/24] read_status, write_status --- CMakeLists.txt | 2 + crosscompile/Dockerfile | 26 ++++++ include/coro/net/read_status.hpp | 12 +++ include/coro/net/tcp/client.hpp | 149 +++++++++++++++++------------- include/coro/net/write_status.hpp | 12 +++ src/net/socket.cpp | 3 +- src/net/tcp/client.cpp | 7 ++ test/net/test_tcp_server.cpp | 32 +++---- 8 files changed, 158 insertions(+), 85 deletions(-) create mode 100644 crosscompile/Dockerfile create mode 100644 include/coro/net/read_status.hpp create mode 100644 include/coro/net/write_status.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c6f7b80f..23e82139 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,8 @@ if(LIBCORO_FEATURE_NETWORKING) include/coro/net/ip_address.hpp src/net/ip_address.cpp include/coro/net/recv_status.hpp src/net/recv_status.cpp include/coro/net/send_status.hpp src/net/send_status.cpp + include/coro/net/write_status.hpp + include/coro/net/read_status.hpp include/coro/net/socket.hpp src/net/socket.cpp include/coro/net/tcp/client.hpp src/net/tcp/client.cpp include/coro/net/tcp/server.hpp src/net/tcp/server.cpp diff --git a/crosscompile/Dockerfile b/crosscompile/Dockerfile new file mode 100644 index 00000000..b7bf0a64 --- /dev/null +++ b/crosscompile/Dockerfile @@ -0,0 +1,26 @@ +FROM ubuntu:22.04 + +RUN apt update && apt install -y \ + mingw-w64 \ + cmake \ + ninja-build \ + zip \ + git \ + g++ \ + build-essential \ + && apt clean + +ARG UID=1000 +ARG GID=1000 + +RUN groupadd -g ${GID} builder && \ + useradd -m -u ${UID} -g ${GID} -s /bin/bash builder + +RUN apt update && apt install -y \ + mingw-w64 \ + cmake \ + ninja-build \ + build-essential + +USER builder +WORKDIR /home/builder/project \ No newline at end of file diff --git a/include/coro/net/read_status.hpp b/include/coro/net/read_status.hpp new file mode 100644 index 00000000..7f4cccc4 --- /dev/null +++ b/include/coro/net/read_status.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace coro::net +{ +enum class read_status +{ + ok, + closed, + timeout, + error +}; +} \ No newline at end of file diff --git a/include/coro/net/tcp/client.hpp b/include/coro/net/tcp/client.hpp index e7ebc9f4..a9b4dcb4 100644 --- a/include/coro/net/tcp/client.hpp +++ b/include/coro/net/tcp/client.hpp @@ -4,9 +4,11 @@ #include "coro/io_scheduler.hpp" #include "coro/net/connect.hpp" #include "coro/net/ip_address.hpp" +#include "coro/net/read_status.hpp" #include "coro/net/recv_status.hpp" #include "coro/net/send_status.hpp" #include "coro/net/socket.hpp" +#include "coro/net/write_status.hpp" #include "coro/platform.hpp" #include "coro/poll.hpp" #include "coro/task.hpp" @@ -76,10 +78,7 @@ class client * event operation is ready. */ auto poll(coro::poll_op op, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> coro::task - { - return m_io_scheduler->poll(m_socket, op, timeout); - } + -> coro::task; /** * Receives incoming data into the given buffer. By default since all tcp client sockets are set @@ -90,31 +89,7 @@ class client * bytes will be a subspan or full span of the given input buffer. */ template - auto recv(buffer_type&& buffer) -> std::pair> - { - // If the user requested zero bytes, just return. - if (buffer.empty()) - { - return {recv_status::ok, std::span{}}; - } - - auto bytes_recv = ::recv(m_socket.native_handle(), buffer.data(), buffer.size(), 0); - if (bytes_recv > 0) - { - // Ok, we've recieved some data. - return {recv_status::ok, std::span{buffer.data(), static_cast(bytes_recv)}}; - } - else if (bytes_recv == 0) - { - // On TCP stream sockets 0 indicates the connection has been closed by the peer. - return {recv_status::closed, std::span{}}; - } - else - { - // Report the error to the user. - return {static_cast(errno), std::span{}}; - } - } + auto recv(buffer_type&& buffer) -> std::pair>; /** * Sends outgoing data from the given buffer. If a partial write occurs then use co_await poll() @@ -127,26 +102,7 @@ class client * were successfully sent the status will be 'ok' and the remaining span will be empty. */ template - auto send(const buffer_type& buffer) -> std::pair> - { - // If the user requested zero bytes, just return. - if (buffer.empty()) - { - return {send_status::ok, std::span{buffer.data(), buffer.size()}}; - } - - auto bytes_sent = ::send(m_socket.native_handle(), buffer.data(), buffer.size(), 0); - if (bytes_sent >= 0) - { - // Some or all of the bytes were written. - return {send_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; - } - else - { - // Due to the error none of the bytes were written. - return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; - } - } + auto send(const buffer_type& buffer) -> std::pair>; #endif /** @@ -161,7 +117,7 @@ class client */ template auto write(const buffer_type& buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> task>>; + -> task>>; /** * Attempts to receive data from the connected peer into the provided buffer. @@ -174,7 +130,7 @@ class client */ template auto read(buffer_type&& buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> std::pair>; + -> task>>; private: /// The tcp::server creates already connected clients and provides a tcp socket pre-built. @@ -192,43 +148,110 @@ class client }; #if defined(CORO_PLATFORM_UNIX) +template +auto client::recv(buffer_type&& buffer) -> std::pair> +{ + // If the user requested zero bytes, just return. + if (buffer.empty()) + { + return {recv_status::ok, std::span{}}; + } + + auto bytes_recv = ::recv(m_socket.native_handle(), buffer.data(), buffer.size(), 0); + if (bytes_recv > 0) + { + // Ok, we've recieved some data. + return {recv_status::ok, std::span{buffer.data(), static_cast(bytes_recv)}}; + } + else if (bytes_recv == 0) + { + // On TCP stream sockets 0 indicates the connection has been closed by the peer. + return {recv_status::closed, std::span{}}; + } + else + { + // Report the error to the user. + return {static_cast(errno), std::span{}}; + } +} + +template +auto client::send(const buffer_type& buffer) -> std::pair> +{ + // If the user requested zero bytes, just return. + if (buffer.empty()) + { + return {send_status::ok, std::span{buffer.data(), buffer.size()}}; + } + + auto bytes_sent = ::send(m_socket.native_handle(), buffer.data(), buffer.size(), 0); + if (bytes_sent >= 0) + { + // Some or all of the bytes were written. + return {send_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; + } + else + { + // Due to the error none of the bytes were written. + return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; + } +} + template auto client::write(const buffer_type& buffer, std::chrono::milliseconds timeout) - -> task>> + -> task>> { if (auto status = co_await poll(poll_op::write, timeout); status != poll_status::event) { switch (status) { case poll_status::closed: - co_return {send_status::closed, std::span{buffer.data(), buffer.size()}}; + co_return {write_status::closed, std::span{buffer.data(), buffer.size()}}; case poll_status::error: - co_return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; + co_return {write_status::error, std::span{buffer.data(), buffer.size()}}; case poll_status::timeout: - // TODO - break; + co_return {write_status::timeout, std::span{buffer.data(), buffer.size()}}; + default: + throw std::runtime_error("Unknown poll_status value."); } } - co_return send(buffer); + switch (auto &&[status, span] = send(std::forward(buffer)); status) + { + case send_status::ok: + co_return {write_status::ok, span}; + case send_status::closed: + co_return {write_status::closed, span}; + default: + co_return {write_status::error, span}; + } } template -auto client::read(buffer_type&& buffer, std::chrono::milliseconds timeout) -> std::pair> +auto client::read(buffer_type&& buffer, std::chrono::milliseconds timeout) -> task>> { if (auto status = co_await poll(poll_op::read, timeout); status != poll_status::event) { switch (status) { case poll_status::closed: - co_return {recv_status::closed, std::span{buffer.data(), buffer.size()}}; + co_return {read_status::closed, std::span{}}; case poll_status::error: - co_return {static_cast(errno), std::span{buffer.data(), buffer.size()}}; + co_return {read_status::error, std::span{}}; case poll_status::timeout: - // TODO - break; + co_return {read_status::timeout, std::span{}}; + default: + throw std::runtime_error("Unknown poll_status value."); } } - co_return recv(std::forward(buffer)); + switch (auto&& [status, span] = recv(std::forward(buffer)); status) + { + case recv_status::ok: + co_return {read_status::ok, span}; + case recv_status::closed: + co_return {read_status::closed, span}; + default: + co_return {read_status::error, span}; + } } #elif defined(CORO_PLATFORM_WINDOWS) #endif diff --git a/include/coro/net/write_status.hpp b/include/coro/net/write_status.hpp new file mode 100644 index 00000000..cd301541 --- /dev/null +++ b/include/coro/net/write_status.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace coro::net +{ +enum class write_status +{ + ok, + closed, + timeout, + error +}; +} \ No newline at end of file diff --git a/src/net/socket.cpp b/src/net/socket.cpp index e5d3855e..be4d7035 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -88,6 +88,7 @@ auto socket::shutdown(poll_op how) -> bool h = SHUT_RDWR; break; } + return (::shutdown(m_fd, h) == 0); #elif defined(_WIN32) || defined(_WIN64) // WinSock uses SD_RECEIVE, SD_SEND, SD_BOTH switch (how) @@ -102,8 +103,8 @@ auto socket::shutdown(poll_op how) -> bool h = SD_BOTH; break; } -#endif return (::shutdown((SOCKET)m_fd, h) == 0); +#endif } auto socket::close() -> void diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index ec141358..cd70f0f5 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -129,4 +129,11 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task coro::task +{ + return m_io_scheduler->poll(m_socket, op, timeout); +} +#endif + } // namespace coro::net::tcp diff --git a/test/net/test_tcp_server.cpp b/test/net/test_tcp_server.cpp index 95686af5..5a9949c9 100644 --- a/test/net/test_tcp_server.cpp +++ b/test/net/test_tcp_server.cpp @@ -27,20 +27,15 @@ TEST_CASE("tcp_server ping server", "[tcp_server]") // Skip polling for write, should really only poll if the write is partial, shouldn't be // required for this test. - std::cerr << "client send()\n"; - auto [sstatus, remaining] = client.send(client_msg); - REQUIRE(sstatus == coro::net::send_status::ok); + std::cerr << "client write()\n"; + auto [sstatus, remaining] = co_await client.write(client_msg); + REQUIRE(sstatus == coro::net::write_status::ok); REQUIRE(remaining.empty()); - // Poll for the server's response. - std::cerr << "client poll(read)\n"; - auto pstatus = co_await client.poll(coro::poll_op::read); - REQUIRE(pstatus == coro::poll_status::event); - std::string buffer(256, '\0'); - std::cerr << "client recv()\n"; - auto [rstatus, rspan] = client.recv(buffer); - REQUIRE(rstatus == coro::net::recv_status::ok); + std::cerr << "client read()\n"; + auto [rstatus, rspan] = co_await client.read(buffer); + REQUIRE(rstatus == coro::net::read_status::ok); REQUIRE(rspan.size() == server_msg.length()); buffer.resize(rspan.size()); REQUIRE(buffer == server_msg); @@ -64,23 +59,18 @@ TEST_CASE("tcp_server ping server", "[tcp_server]") auto client = server.accept(); REQUIRE(client.socket().is_valid()); - // Poll for client request. - std::cerr << "server poll(read)\n"; - pstatus = co_await client.poll(coro::poll_op::read); - REQUIRE(pstatus == coro::poll_status::event); - std::string buffer(256, '\0'); - std::cerr << "server recv()\n"; - auto [rstatus, rspan] = client.recv(buffer); - REQUIRE(rstatus == coro::net::recv_status::ok); + std::cerr << "server read()\n"; + auto [rstatus, rspan] = co_await client.read(buffer); + REQUIRE(rstatus == coro::net::read_status::ok); REQUIRE(rspan.size() == client_msg.size()); buffer.resize(rspan.size()); REQUIRE(buffer == client_msg); // Respond to client. std::cerr << "server send()\n"; - auto [sstatus, remaining] = client.send(server_msg); - REQUIRE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await client.write(server_msg); + REQUIRE(sstatus == coro::net::write_status::ok); REQUIRE(remaining.empty()); std::cerr << "server return\n"; From d6ff687fe2f8b54369b3a3469ca31189369fc11d Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sun, 22 Jun 2025 20:46:47 +0300 Subject: [PATCH 11/24] Client accept --- src/net/tcp/client.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index cd70f0f5..942b4852 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -7,8 +7,9 @@ using namespace std::chrono_literals; client::client(std::shared_ptr scheduler, options opts) : m_io_scheduler(std::move(scheduler)), m_options(std::move(opts)), - m_socket(net::make_socket( - net::socket::options{m_options.address.domain(), net::socket::type_t::tcp, net::socket::blocking_t::no})) + m_socket( + net::make_socket( + net::socket::options{m_options.address.domain(), net::socket::type_t::tcp, net::socket::blocking_t::no})) { if (m_io_scheduler == nullptr) { @@ -93,6 +94,7 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task(m_options.address.data().data()); +#if defined(CORO_PLATFORM_UNIX) auto cret = ::connect(m_socket.native_handle(), reinterpret_cast(&server), sizeof(server)); if (cret == 0) { @@ -127,6 +129,9 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task Date: Sat, 5 Jul 2025 20:15:35 +0300 Subject: [PATCH 12/24] IOCP write/read, enum for completion keys, IOCP timers --- CMakeLists.txt | 1 + include/coro/detail/io_notifier_iocp.hpp | 28 ++-- include/coro/detail/iocp_overlapped.hpp | 17 +++ include/coro/detail/signal_win32.hpp | 2 - include/coro/detail/timer_handle.hpp | 18 ++- include/coro/io_scheduler.hpp | 7 +- include/coro/net/tcp/client.hpp | 25 ++-- src/detail/io_notifier_iocp.cpp | 148 +++++++++++++++----- src/detail/signal_win32.cpp | 16 ++- src/detail/timer_handle.cpp | 32 ++++- src/io_scheduler.cpp | 31 +++++ src/net/tcp/client.cpp | 166 ++++++++++++++++++++--- 12 files changed, 407 insertions(+), 84 deletions(-) create mode 100644 include/coro/detail/iocp_overlapped.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 23e82139..6019975e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ set(LIBCORO_SOURCE_FILES include/coro/generator.hpp include/coro/latch.hpp include/coro/mutex.hpp src/mutex.cpp + include/coro/platform.hpp include/coro/queue.hpp include/coro/ring_buffer.hpp include/coro/semaphore.hpp src/semaphore.cpp diff --git a/include/coro/detail/io_notifier_iocp.hpp b/include/coro/detail/io_notifier_iocp.hpp index 5f08a4c4..4c5b08d8 100644 --- a/include/coro/detail/io_notifier_iocp.hpp +++ b/include/coro/detail/io_notifier_iocp.hpp @@ -7,8 +7,18 @@ namespace coro::detail { +class timer_handle; + class io_notifier_iocp { +public: + enum class completion_key : unsigned long long + { + signal, + socket, + timer + }; + public: io_notifier_iocp(); @@ -19,26 +29,28 @@ class io_notifier_iocp ~io_notifier_iocp(); - //auto watch_timer(const detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool; + auto watch_timer(const detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool; auto watch(const coro::signal& signal, void* data) -> bool; - auto watch(detail::poll_info& pi) -> bool; - - auto unwatch(detail::poll_info& pi) -> bool; - - //auto unwatch_timer(const detail::timer_handle& timer) -> bool; + auto unwatch_timer(const detail::timer_handle& timer) -> bool; auto next_events( - std::vector>& ready_events, std::chrono::milliseconds timeout) - -> void; + std::vector>& ready_events, + std::chrono::milliseconds timeout + ) -> void; //static auto event_to_poll_status(const event_t& event) -> poll_status; + auto iocp() const noexcept -> void* { return m_iocp; } + private: void* m_iocp{}; void set_signal_active(void* data, bool active); + void process_active_signals( + std::vector>& ready_events + ); std::mutex m_active_signals_mutex; std::vector m_active_signals; diff --git a/include/coro/detail/iocp_overlapped.hpp b/include/coro/detail/iocp_overlapped.hpp new file mode 100644 index 00000000..08b8dabe --- /dev/null +++ b/include/coro/detail/iocp_overlapped.hpp @@ -0,0 +1,17 @@ +// NOTE: This file includes , which pulls in many global symbols. +// Do not include this file from headers. Include only in implementation files (.cpp) or modules. + +#pragma once +#include +#include +#include + +namespace coro::detail +{ +struct overlapped_io_operation +{ + OVERLAPPED ov{}; + poll_info pi; + std::size_t bytes_transferred{}; +}; +} \ No newline at end of file diff --git a/include/coro/detail/signal_win32.hpp b/include/coro/detail/signal_win32.hpp index 4342f1c1..c2eef532 100644 --- a/include/coro/detail/signal_win32.hpp +++ b/include/coro/detail/signal_win32.hpp @@ -20,7 +20,5 @@ class signal_win32 mutable void* m_iocp{}; mutable void* m_data{}; std::unique_ptr m_event; - - static constexpr const int signal_key = 1; }; } // namespace coro::detail \ No newline at end of file diff --git a/include/coro/detail/timer_handle.hpp b/include/coro/detail/timer_handle.hpp index 4d97d0f5..5fb50f2f 100644 --- a/include/coro/detail/timer_handle.hpp +++ b/include/coro/detail/timer_handle.hpp @@ -11,15 +11,29 @@ namespace detail class timer_handle { - coro::fd_t m_fd; +#if defined(CORO_PLATFORM_UNIX) + using native_handle_t = coro::fd_t; +#elif defined(CORO_PLATFORM_WINDOWS) + friend class io_notifier_iocp; + using native_handle_t = void *; + + mutable void* m_iocp = nullptr; +#endif + + native_handle_t m_native_handle; const void* m_timer_handle_ptr = nullptr; public: timer_handle(const void* timer_handle_ptr, io_notifier& notifier); + ~timer_handle(); - coro::fd_t get_fd() const { return m_fd; } + native_handle_t get_native_handle() const { return m_native_handle; } const void* get_inner() const { return m_timer_handle_ptr; } + +#if defined(CORO_PLATFORM_WINDOWS) + void* get_iocp() const { return m_iocp; } +#endif }; } // namespace detail diff --git a/include/coro/io_scheduler.hpp b/include/coro/io_scheduler.hpp index db3f2862..b4e684f8 100644 --- a/include/coro/io_scheduler.hpp +++ b/include/coro/io_scheduler.hpp @@ -322,6 +322,7 @@ class io_scheduler : public std::enable_shared_from_this */ [[nodiscard]] auto yield_until(time_point time) -> coro::task; +#if defined(CORO_PLATFORM_UNIX) /** * Polls the given file descriptor for the given operations. * @param fd The file descriptor to poll for events. @@ -333,7 +334,7 @@ class io_scheduler : public std::enable_shared_from_this [[nodiscard]] auto poll(fd_t fd, coro::poll_op op, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task; -#ifdef LIBCORO_FEATURE_NETWORKING + #ifdef LIBCORO_FEATURE_NETWORKING /** * Polls the given coro::net::socket for the given operations. * @param sock The socket to poll for events on. @@ -348,6 +349,10 @@ class io_scheduler : public std::enable_shared_from_this { return poll(sock.native_handle(), op, timeout); } + #endif +#elif defined(CORO_PLATFORM_WINDOWS) && defined(LIBCORO_FEATURE_NETWORKING) + auto poll(detail::poll_info& pi, std::chrono::milliseconds timeout) -> coro::task; + auto bind_socket(const net::socket& sock) -> void; #endif /** diff --git a/include/coro/net/tcp/client.hpp b/include/coro/net/tcp/client.hpp index a9b4dcb4..17499c95 100644 --- a/include/coro/net/tcp/client.hpp +++ b/include/coro/net/tcp/client.hpp @@ -45,12 +45,18 @@ class client .address = {net::ip_address::from_string("127.0.0.1")}, .port = 8080, }); - client(const client& other); client(client&& other) noexcept; - auto operator=(const client& other) noexcept -> client&; auto operator=(client&& other) noexcept -> client&; ~client(); +#if defined(CORO_PLATFORM_UNIX) + client(const client& other); + auto operator=(const client& other) noexcept -> client&; +#elif defined(CORO_PLATFORM_WINDOWS) + client(const client& other) = delete; + auto operator=(const client& other) noexcept -> client& = delete; +#endif + /** * @return The tcp socket this client is using. * @{ @@ -103,6 +109,7 @@ class client */ template auto send(const buffer_type& buffer) -> std::pair>; + #endif /** @@ -115,8 +122,7 @@ class client * @param timeout Maximum time to wait for the operation to complete. Zero means no timeout. * @return A pair containing the status and a span of any unsent data. If successful, the span will be empty. */ - template - auto write(const buffer_type& buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + auto write(std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> task>>; /** @@ -128,8 +134,7 @@ class client * @param timeout Maximum time to wait for the operation to complete. Zero means no timeout. * @return A pair containing the status and a span of received bytes. The span may be empty. */ - template - auto read(buffer_type&& buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + auto read(std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> task>>; private: @@ -148,7 +153,6 @@ class client }; #if defined(CORO_PLATFORM_UNIX) -template auto client::recv(buffer_type&& buffer) -> std::pair> { // If the user requested zero bytes, just return. @@ -197,8 +201,7 @@ auto client::send(const buffer_type& buffer) -> std::pair -auto client::write(const buffer_type& buffer, std::chrono::milliseconds timeout) +auto client::write(std::span buffer, std::chrono::milliseconds timeout) -> task>> { if (auto status = co_await poll(poll_op::write, timeout); status != poll_status::event) @@ -226,8 +229,7 @@ auto client::write(const buffer_type& buffer, std::chrono::milliseconds timeout) } } -template -auto client::read(buffer_type&& buffer, std::chrono::milliseconds timeout) -> task>> +auto client::read(std::span buffer, std::chrono::milliseconds timeout) -> task>> { if (auto status = co_await poll(poll_op::read, timeout); status != poll_status::event) { @@ -253,7 +255,6 @@ auto client::read(buffer_type&& buffer, std::chrono::milliseconds timeout) -> ta co_return {read_status::error, span}; } } -#elif defined(CORO_PLATFORM_WINDOWS) #endif } // namespace coro::net::tcp diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp index f51a2a9f..7addf0f7 100644 --- a/src/detail/io_notifier_iocp.cpp +++ b/src/detail/io_notifier_iocp.cpp @@ -1,20 +1,23 @@ #include "coro/detail/io_notifier_iocp.hpp" #include +#include #include "coro/detail/signal_win32.hpp" +#include "coro/detail/timer_handle.hpp" +#include namespace coro::detail { struct signal_win32::Event { - OVERLAPPED overlapped; void* data; bool is_set; }; - io_notifier_iocp::io_notifier_iocp() { - m_iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); + std::size_t concurrent_threads = 0; // TODO + + m_iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, concurrent_threads); } io_notifier_iocp::~io_notifier_iocp() @@ -22,25 +25,68 @@ io_notifier_iocp::~io_notifier_iocp() CloseHandle(m_iocp); } +static VOID CALLBACK onTimerFired(LPVOID timerPtr, DWORD, DWORD) +{ + auto* handle = static_cast(timerPtr); + + // Completion key 3 means timer + PostQueuedCompletionStatus(handle->get_iocp(), 0, 3, static_cast(const_cast(handle->get_inner()))); +} + +auto io_notifier_iocp::watch_timer(const detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool +{ + if (timer.m_iocp == nullptr) { + timer.m_iocp = m_iocp; + } + else if (timer.m_iocp != m_iocp) + { + throw std::runtime_error("Timer is already associated with a different IOCP handle. Cannot reassign."); + } + + LARGE_INTEGER dueTime{}; + // time in 100ns intervals, negative for relative + dueTime.QuadPart = -duration.count() / 100; + + // `timer_handle` must remain alive until the timer fires. + // This is guaranteed by `io_scheduler`, which owns the timer lifetime. + // + // We could allocate a separate `timer_context` on the heap to decouple ownership, + // but safely freeing it is difficult without introducing overhead or leaks: + // the timer can be cancelled, and we have no guaranteed way to retrieve the pointer. + // + // Therefore, we directly pass a pointer to `timer_handle` as the APC context. + // This avoids allocations and should be safe (I hope) under our scheduler's lifetime guarantees. + return SetWaitableTimer(timer.get_native_handle(), &dueTime, 0, &onTimerFired, (void*)std::addressof(timer), FALSE); +} + +auto io_notifier_iocp::unwatch_timer(const detail::timer_handle& timer) -> bool +{ + return CancelWaitableTimer(timer.get_native_handle()); +} + auto io_notifier_iocp::watch(const coro::signal& signal, void* data) -> bool { signal.m_iocp = m_iocp; signal.m_data = data; + return true; } auto io_notifier_iocp::next_events( std::vector>& ready_events, - std::chrono::milliseconds timeout + const std::chrono::milliseconds timeout ) -> void { - DWORD bytesTransferred = 0; - ULONG_PTR completionKey = 0; + DWORD bytes_transferred = 0; + completion_key completion_key; LPOVERLAPPED overlapped = nullptr; DWORD timeoutMs = (timeout.count() == -1) ? INFINITE : static_cast(timeout.count()); + process_active_signals(ready_events); + while (true) { - BOOL success = GetQueuedCompletionStatus(m_iocp, &bytesTransferred, &completionKey, &overlapped, timeoutMs); + BOOL success = GetQueuedCompletionStatus( + m_iocp, &bytes_transferred, reinterpret_cast(std::addressof(completion_key)), &overlapped, timeoutMs); if (!success && overlapped == nullptr) { @@ -48,43 +94,69 @@ auto io_notifier_iocp::next_events( return; } - if (completionKey == signal_win32::signal_key) + switch (completion_key) { - if (!overlapped) - continue; - - auto* event = reinterpret_cast(overlapped); - set_signal_active(event->data, event->is_set); - continue; - } + case completion_key::signal: + { + if (!overlapped) + continue; - if (completionKey == 2) // socket - { - auto* info = reinterpret_cast(completionKey); - if (!info) + auto* event = reinterpret_cast(overlapped); + set_signal_active(event->data, event->is_set); + if (event->is_set) + { + // poll_status doesn't matter. + ready_events.emplace_back(event->data, coro::poll_status::event); + } continue; - coro::poll_status status; + } + case completion_key::socket: + { + auto* info = reinterpret_cast(overlapped); + if (!info) + continue; + + info->bytes_transferred = bytes_transferred; + + coro::poll_status status; - if (!success) { - DWORD err = GetLastError(); - if (err == ERROR_NETNAME_DELETED || err == ERROR_CONNECTION_ABORTED || err == ERROR_OPERATION_ABORTED) + if (!success) + { + DWORD err = GetLastError(); + if (err == ERROR_NETNAME_DELETED || err == ERROR_CONNECTION_ABORTED || + err == ERROR_OPERATION_ABORTED) + status = coro::poll_status::closed; + else + status = coro::poll_status::error; + } + else if (bytes_transferred == 0) + { + // The connection is closed normally status = coro::poll_status::closed; + } else - status = coro::poll_status::error; + { + status = coro::poll_status::event; + } + + ready_events.emplace_back(&info->pi, status); + continue; } - else if (bytesTransferred == 0) { - // The connection is closed normally - status = coro::poll_status::closed; + case completion_key::timer: // timer + { + // Remember that it's not real poll_info, it's just a pointer to some random data + // io_scheduler must handle it. + // poll_status doesn't matter. + auto* handle_ptr = reinterpret_cast(overlapped); + ready_events.emplace_back(handle_ptr, coro::poll_status::event); + break; } - else { - status = coro::poll_status::event; + default: + { + throw std::runtime_error("Received unknown completion key."); } - - ready_events.emplace_back(info, status); - continue; } - throw std::runtime_error("Received unknown completion key."); } } void io_notifier_iocp::set_signal_active(void* data, bool active) @@ -97,4 +169,14 @@ void io_notifier_iocp::set_signal_active(void* data, bool active) m_active_signals.erase(std::remove(std::begin(m_active_signals), std::end(m_active_signals), data)); } } +void io_notifier_iocp::process_active_signals( + std::vector>& ready_events +) +{ + for (void* data : m_active_signals) + { + // poll_status doesn't matter. + ready_events.emplace_back(data, poll_status::event); + } +} } \ No newline at end of file diff --git a/src/detail/signal_win32.cpp b/src/detail/signal_win32.cpp index f485a023..42750128 100644 --- a/src/detail/signal_win32.cpp +++ b/src/detail/signal_win32.cpp @@ -1,4 +1,5 @@ #include +#include #include namespace coro::detail @@ -6,7 +7,6 @@ namespace coro::detail // Maybe not thread-safe struct signal_win32::Event { - OVERLAPPED overlapped; void* data; bool is_set; }; @@ -22,12 +22,22 @@ void signal_win32::set() { m_event->is_set = true; m_event->data = m_data; - PostQueuedCompletionStatus(m_iocp, 0, (ULONG_PTR)signal_key, (LPOVERLAPPED)(void*)m_event.get()); + PostQueuedCompletionStatus( + m_iocp, + 0, + (ULONG_PTR)io_notifier::completion_key::signal, + (LPOVERLAPPED)(void*)m_event.get() + ); } void signal_win32::unset() { m_event->is_set = false; m_event->data = m_data; - PostQueuedCompletionStatus(m_iocp, 0, (ULONG_PTR)signal_key, (LPOVERLAPPED)(void*)m_event.get()); + PostQueuedCompletionStatus( + m_iocp, + 0, + (ULONG_PTR)io_notifier::completion_key::signal, + (LPOVERLAPPED)(void*)m_event.get() + ); } } \ No newline at end of file diff --git a/src/detail/timer_handle.cpp b/src/detail/timer_handle.cpp index a65517f8..651e4aff 100644 --- a/src/detail/timer_handle.cpp +++ b/src/detail/timer_handle.cpp @@ -1,30 +1,52 @@ #include "coro/detail/timer_handle.hpp" #include "coro/io_notifier.hpp" +#include namespace coro::detail { -#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) +#if defined(CORO_PLATFORM_BSD) static auto kqueue_current_timer_fd = std::atomic{0}; timer_handle::timer_handle(const void* timer_handle_ptr, io_notifier& notifier) - : m_fd{kqueue_current_timer_fd++}, + : m_native_handle{kqueue_current_timer_fd++}, m_timer_handle_ptr(timer_handle_ptr) { (void)notifier; } +timer_handle::~timer_handle() +{ +} -#elif defined(__linux__) +#elif defined(CORO_PLATFORM_LINUX) timer_handle::timer_handle(const void* timer_handle_ptr, io_notifier& notifier) - : m_fd(::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC)), + : m_native_handle(::timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC)), m_timer_handle_ptr(timer_handle_ptr) { - notifier.watch(m_fd, coro::poll_op::read, const_cast(m_timer_handle_ptr), true); + notifier.watch(m_native_handle, coro::poll_op::read, const_cast(m_timer_handle_ptr), true); } +timer_handle::~timer_handle() +{ +} + +#elif defined(CORO_PLATFORM_WINDOWS) +timer_handle::timer_handle(const void* timer_handle_ptr, io_notifier& notifier) + : m_timer_handle_ptr(timer_handle_ptr), + m_native_handle(CreateWaitableTimer(NULL, FALSE, NULL)) +{ + if (m_native_handle == NULL) + { + throw std::runtime_error("Failed to CreateWaitableTimer"); + } +} +timer_handle::~timer_handle() +{ + CloseHandle(m_native_handle); +} #endif } // namespace coro::detail diff --git a/src/io_scheduler.cpp b/src/io_scheduler.cpp index 786449e9..20ea21f2 100644 --- a/src/io_scheduler.cpp +++ b/src/io_scheduler.cpp @@ -102,6 +102,8 @@ auto io_scheduler::yield_until(time_point time) -> coro::task co_return; } +#if defined(CORO_PLATFORM_UNIX) + auto io_scheduler::poll(fd_t fd, coro::poll_op op, std::chrono::milliseconds timeout) -> coro::task { // Because the size will drop when this coroutine suspends every poll needs to undo the subtraction @@ -134,6 +136,35 @@ auto io_scheduler::poll(fd_t fd, coro::poll_op op, std::chrono::milliseconds tim co_return result; } +#elif defined(CORO_PLATFORM_WINDOWS) && defined(LIBCORO_FEATURE_NETWORKING) +auto io_scheduler::poll(detail::poll_info& pi, std::chrono::milliseconds timeout) -> coro::task +{ + m_size.fetch_add(1, std::memory_order::release); + bool timeout_requested = (timeout > 0ms); + + if (timeout_requested) + { + pi.m_timer_pos = add_timer_token(clock::now() + timeout, pi); + } + + auto result = co_await pi; + + m_size.fetch_sub(1, std::memory_order::release); + co_return result; +} +auto io_scheduler::bind_socket(const net::socket& sock) -> void +{ + std::size_t concurrent_threads = m_thread_pool ? m_thread_pool->size() : 0; + HANDLE handle = CreateIoCompletionPort( + reinterpret_cast(sock.native_handle()), + m_io_notifier.iocp(), + static_cast(io_notifier::completion_key::socket), + concurrent_threads + ); + // TODO: check handle +} +#endif + auto io_scheduler::shutdown() noexcept -> void { // Only allow shutdown to occur once. diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index 942b4852..b3186358 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -1,4 +1,6 @@ #include "coro/net/tcp/client.hpp" +#include +#include namespace coro::net::tcp { @@ -15,6 +17,11 @@ client::client(std::shared_ptr scheduler, options opts) { throw std::runtime_error{"tcp::client cannot have nullptr io_scheduler"}; } + +#if defined(CORO_PLATFORM_WINDOWS) + // Bind socket to IOCP + m_io_scheduler->bind_socket(m_socket); +#endif } client::client(std::shared_ptr scheduler, net::socket socket, options opts) @@ -29,14 +36,6 @@ client::client(std::shared_ptr scheduler, net::socket socket, opti m_socket.blocking(coro::net::socket::blocking_t::no); } -client::client(const client& other) - : m_io_scheduler(other.m_io_scheduler), - m_options(other.m_options), - m_socket(other.m_socket), - m_connect_status(other.m_connect_status) -{ -} - client::client(client&& other) noexcept : m_io_scheduler(std::move(other.m_io_scheduler)), m_options(std::move(other.m_options)), @@ -49,29 +48,40 @@ client::~client() { } -auto client::operator=(const client& other) noexcept -> client& +auto client::operator=(client&& other) noexcept -> client& { if (std::addressof(other) != this) { - m_io_scheduler = other.m_io_scheduler; - m_options = other.m_options; - m_socket = other.m_socket; - m_connect_status = other.m_connect_status; + m_io_scheduler = std::move(other.m_io_scheduler); + m_options = std::move(other.m_options); + m_socket = std::move(other.m_socket); + m_connect_status = std::exchange(other.m_connect_status, std::nullopt); } return *this; } -auto client::operator=(client&& other) noexcept -> client& + +#if defined(CORO_PLATFORM_UNIX) +client::client(const client& other) + : m_io_scheduler(other.m_io_scheduler), + m_options(other.m_options), + m_socket(other.m_socket), + m_connect_status(other.m_connect_status) +{ +} + +auto client::operator=(const client& other) noexcept -> client& { if (std::addressof(other) != this) { - m_io_scheduler = std::move(other.m_io_scheduler); - m_options = std::move(other.m_options); - m_socket = std::move(other.m_socket); - m_connect_status = std::exchange(other.m_connect_status, std::nullopt); + m_io_scheduler = other.m_io_scheduler; + m_options = other.m_options; + m_socket = other.m_socket; + m_connect_status = other.m_connect_status; } return *this; } +#endif auto client::connect(std::chrono::milliseconds timeout) -> coro::task { @@ -130,7 +140,57 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task(&server), + sizeof(server), + nullptr, + 0, + 0, + &ovpi.ov + ); + + if (res) + { + setsockopt(m_socket.native_handle(), SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0); + co_return return_value(connect_status::connected); + } + + if (WSAGetLastError() == WSA_IO_PENDING) + { + auto status = co_await m_io_scheduler->poll(ovpi.pi, timeout); + if (status == poll_status::event) + { + co_return return_value(connect_status::connected); + } + else if (status == poll_status::timeout) + { + CancelIoEx((HANDLE)m_socket.native_handle(), &ovpi.ov); + co_return return_value(connect_status::timeout); + } + } + + co_return return_value(connect_status::error); #endif } @@ -139,6 +199,76 @@ auto client::poll(coro::poll_op op, std::chrono::milliseconds timeout) -> coro:: { return m_io_scheduler->poll(m_socket, op, timeout); } +#elif defined(CORO_PLATFORM_WINDOWS) + +auto client::write(std::span buffer, std::chrono::milliseconds timeout) + -> task>> +{ + detail::overlapped_io_operation ov{}; + WSABUF buf; + buf.buf = const_cast(buffer.data()); + buf.len = buffer.size(); + DWORD flags = 0, bytes_sent = 0; + + int r = WSASend(m_socket.native_handle(), &buf, 1, &bytes_sent, flags, &ov.ov, nullptr); + if (r == 0) // Data already sent + { + if (bytes_sent == 0) + co_return {write_status::closed, buffer}; + co_return {write_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; + } + else if (WSAGetLastError() == WSA_IO_PENDING) + { + auto status = co_await m_io_scheduler->poll(ov.pi, timeout); + bytes_sent = ov.bytes_transferred; + if (status == poll_status::event) + { + co_return {write_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; + } + else if (status == poll_status::timeout) + { + CancelIoEx((HANDLE)m_socket.native_handle(), &ov.ov); + co_return {write_status::timeout, buffer}; + } + } + + + co_return {write_status::error, buffer}; +} + +auto client::read(std::span buffer, std::chrono::milliseconds timeout) + -> task>> +{ + detail::overlapped_io_operation ov{}; + WSABUF buf; + buf.buf = buffer.data(); + buf.len = buffer.size(); + DWORD flags = 0, bytes_recv = 0; + + int r = WSARecv(m_socket.native_handle(), &buf, 1, &bytes_recv, &flags, &ov.ov, nullptr); + if (r == 0) // Data already read + { + if (bytes_recv == 0) + co_return {read_status::closed, buffer}; + co_return {read_status::ok, std::span{buffer.data(), bytes_recv}}; + } + else if (WSAGetLastError() == WSA_IO_PENDING) + { + auto status = co_await m_io_scheduler->poll(ov.pi, timeout); + bytes_recv = ov.bytes_transferred; + if (status == poll_status::event) + { + co_return {read_status::ok, std::span{buffer.data(), bytes_recv}}; + } + else if (status == poll_status::timeout) + { + CancelIoEx((HANDLE)m_socket.native_handle(), &ov.ov); + co_return {read_status::timeout, std::span{}}; + } + } + + co_return {read_status::error, std::span{}}; +} #endif } // namespace coro::net::tcp From 79268e9af5372e070d16399c4c61c1ec24b5a3b2 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Tue, 8 Jul 2025 19:30:28 +0300 Subject: [PATCH 13/24] IOCP support for TCP networking winsock_handle RAII wrapper for WinSock startup/cleanup Refactored socket and I/O logic to use completion ports Crossplatform asynchronous accept_client, connect, read, and write Updated the TCP echo server example to use the new IOCP API Added Windows-specific CMake definitions and compiler flags Temporarily removed UDP support during refactor --- CMakeLists.txt | 3 +- examples/CMakeLists.txt | 1 + examples/coro_tcp_echo_server.cpp | 38 ++-- include/coro/coro.hpp | 2 +- include/coro/detail/io_notifier_iocp.hpp | 27 ++- include/coro/detail/iocp_overlapped.hpp | 17 +- include/coro/detail/poll_info.hpp | 6 +- include/coro/detail/signal_win32.hpp | 7 +- include/coro/detail/winsock_handle.hpp | 42 +++++ include/coro/fd.hpp | 6 + include/coro/io_scheduler.hpp | 3 +- include/coro/net/ip_address.hpp | 55 +----- include/coro/net/socket.hpp | 22 ++- include/coro/net/tcp/client.hpp | 2 +- include/coro/net/tcp/server.hpp | 15 +- include/coro/poll.hpp | 15 +- src/detail/io_notifier_iocp.cpp | 221 +++++++++++++---------- src/detail/signal_win32.cpp | 22 +-- src/detail/winsock_handle.cpp | 40 ++++ src/io_scheduler.cpp | 20 +- src/net/ip_address.cpp | 61 +++++++ src/net/socket.cpp | 38 ++-- src/net/tcp/client.cpp | 60 +++--- src/net/tcp/server.cpp | 164 ++++++++++++++++- src/net/tls/client.cpp | 2 +- test/CMakeLists.txt | 6 +- 26 files changed, 600 insertions(+), 295 deletions(-) create mode 100644 include/coro/detail/winsock_handle.hpp create mode 100644 src/detail/winsock_handle.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6019975e..b5c05d2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,7 @@ if(LIBCORO_FEATURE_NETWORKING) list(APPEND LIBCORO_SOURCE_FILES include/coro/detail/io_notifier_iocp.hpp src/detail/io_notifier_iocp.cpp include/coro/detail/signal_win32.hpp src/detail/signal_win32.cpp + include/coro/detail/winsock_handle.hpp src/detail/winsock_handle.cpp ) endif() @@ -157,7 +158,7 @@ if(LIBCORO_FEATURE_NETWORKING) include/coro/net/socket.hpp src/net/socket.cpp include/coro/net/tcp/client.hpp src/net/tcp/client.cpp include/coro/net/tcp/server.hpp src/net/tcp/server.cpp - include/coro/net/udp/peer.hpp src/net/udp/peer.cpp + # include/coro/net/udp/peer.hpp src/net/udp/peer.cpp ) if(LIBCORO_FEATURE_TLS) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4adc6700..6ec2bc22 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -6,6 +6,7 @@ if(${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") elseif(${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") set(LIBCORO_EXAMPLE_OPTIONS -Wall -Wextra -pipe) elseif(MSVC) + add_compile_definitions(NOMINMAX) set(LIBCORO_EXAMPLE_OPTIONS /W4) else() message(FATAL_ERROR "Unsupported compiler.") diff --git a/examples/coro_tcp_echo_server.cpp b/examples/coro_tcp_echo_server.cpp index 8a1e2981..2ca65626 100644 --- a/examples/coro_tcp_echo_server.cpp +++ b/examples/coro_tcp_echo_server.cpp @@ -10,19 +10,13 @@ auto main() -> int while (true) { - // Wait for data to be available to read. - co_await client.poll(coro::poll_op::read); - auto [rstatus, rspan] = client.recv(buf); + auto [rstatus, rspan] = co_await client.read(buf); switch (rstatus) { - case coro::net::recv_status::ok: - // Make sure the client socket can be written to. - co_await client.poll(coro::poll_op::write); - client.send(std::span{rspan}); + case coro::net::read_status::ok: + co_await client.write(rspan); break; - case coro::net::recv_status::would_block: - break; - case coro::net::recv_status::closed: + case coro::net::read_status::closed: default: co_return; } @@ -34,24 +28,14 @@ auto main() -> int while (true) { - // Wait for a new connection. - auto pstatus = co_await server.poll(); - switch (pstatus) + auto client = co_await server.accept_client(); + if (client && client->socket().is_valid()) { - case coro::poll_status::event: - { - auto client = server.accept(); - if (client.socket().is_valid()) - { - scheduler->spawn(make_on_connection_task(std::move(client))); - } // else report error or something if the socket was invalid or could not be accepted. - } - break; - case coro::poll_status::error: - case coro::poll_status::closed: - case coro::poll_status::timeout: - default: - co_return; + scheduler->spawn(make_on_connection_task(std::move(*client))); + } + else + { + co_return; } } diff --git a/include/coro/coro.hpp b/include/coro/coro.hpp index 0dbcd1a0..3a5f3ab2 100644 --- a/include/coro/coro.hpp +++ b/include/coro/coro.hpp @@ -26,7 +26,7 @@ #include "coro/net/recv_status.hpp" #include "coro/net/send_status.hpp" #include "coro/net/socket.hpp" - #include "coro/net/udp/peer.hpp" +// #include "coro/net/udp/peer.hpp" #endif #include "coro/condition_variable.hpp" diff --git a/include/coro/detail/io_notifier_iocp.hpp b/include/coro/detail/io_notifier_iocp.hpp index 4c5b08d8..1d36698f 100644 --- a/include/coro/detail/io_notifier_iocp.hpp +++ b/include/coro/detail/io_notifier_iocp.hpp @@ -1,9 +1,9 @@ #pragma once -#include #include "coro/detail/poll_info.hpp" #include "coro/fd.hpp" #include "coro/poll.hpp" #include "coro/signal.hpp" +#include namespace coro::detail { @@ -14,7 +14,8 @@ class io_notifier_iocp public: enum class completion_key : unsigned long long { - signal, + signal_set, + signal_unset, socket, timer }; @@ -22,10 +23,10 @@ class io_notifier_iocp public: io_notifier_iocp(); - io_notifier_iocp(const io_notifier_iocp&) = delete; - io_notifier_iocp(io_notifier_iocp&&) = delete; - auto operator=(const io_notifier_iocp&) -> io_notifier_iocp& = delete; - auto operator=(io_notifier_iocp&&) -> io_notifier_iocp& = delete; + io_notifier_iocp(const io_notifier_iocp&) = delete; + io_notifier_iocp(io_notifier_iocp&&) = delete; + auto operator=(const io_notifier_iocp&) -> io_notifier_iocp& = delete; + auto operator=(io_notifier_iocp&&) -> io_notifier_iocp& = delete; ~io_notifier_iocp(); @@ -36,11 +37,11 @@ class io_notifier_iocp auto unwatch_timer(const detail::timer_handle& timer) -> bool; auto next_events( - std::vector>& ready_events, - std::chrono::milliseconds timeout - ) -> void; + std::vector>& ready_events, + std::chrono::milliseconds timeout, + size_t max_events = 16) -> void; - //static auto event_to_poll_status(const event_t& event) -> poll_status; + // static auto event_to_poll_status(const event_t& event) -> poll_status; auto iocp() const noexcept -> void* { return m_iocp; } @@ -48,11 +49,9 @@ class io_notifier_iocp void* m_iocp{}; void set_signal_active(void* data, bool active); - void process_active_signals( - std::vector>& ready_events - ); + void process_active_signals(std::vector>& ready_events); std::mutex m_active_signals_mutex; std::vector m_active_signals; }; -} \ No newline at end of file +} // namespace coro::detail \ No newline at end of file diff --git a/include/coro/detail/iocp_overlapped.hpp b/include/coro/detail/iocp_overlapped.hpp index 08b8dabe..09646078 100644 --- a/include/coro/detail/iocp_overlapped.hpp +++ b/include/coro/detail/iocp_overlapped.hpp @@ -2,16 +2,23 @@ // Do not include this file from headers. Include only in implementation files (.cpp) or modules. #pragma once -#include #include -#include +#include namespace coro::detail { struct overlapped_io_operation { - OVERLAPPED ov{}; + OVERLAPPED ov{}; // Base Windows OVERLAPPED structure for async I/O poll_info pi; - std::size_t bytes_transferred{}; + std::size_t bytes_transferred{}; // Number of bytes read or written once the operation completes + + /** + * Marks this operation as an AcceptEx call: + * allows distinguishing accept events from normal reads/writes + * and ensures that a zero-byte completion isn’t treated as poll_status::closed + */ + bool is_accept{}; }; -} \ No newline at end of file + +} // namespace coro::detail \ No newline at end of file diff --git a/include/coro/detail/poll_info.hpp b/include/coro/detail/poll_info.hpp index 94252146..ab0f85f5 100644 --- a/include/coro/detail/poll_info.hpp +++ b/include/coro/detail/poll_info.hpp @@ -1,8 +1,8 @@ #pragma once #include "coro/fd.hpp" -#include "coro/poll.hpp" #include "coro/time.hpp" +#include "coro/poll.hpp" #include #include @@ -31,7 +31,9 @@ struct poll_info poll_info() = default; ~poll_info() = default; + #if defined(CORO_PLATFORM_UNIX) poll_info(fd_t fd, coro::poll_op op) : m_fd(fd), m_op(op) {} + #endif poll_info(const poll_info&) = delete; poll_info(poll_info&&) = delete; @@ -55,11 +57,13 @@ struct poll_info auto operator co_await() noexcept -> poll_awaiter { return poll_awaiter{*this}; } +#if defined(CORO_PLATFORM_UNIX) /// The file descriptor being polled on. This is needed so that if the timeout occurs first then /// the event loop can immediately disable the event within epoll. fd_t m_fd{-1}; /// The operation that is being waited for to be performed on the file descriptor. coro::poll_op m_op; +#endif /// The timeout's position in the timeout map. A poll() with no timeout or yield() this is empty. /// This is needed so that if the event occurs first then the event loop can immediately disable /// the timeout within epoll. diff --git a/include/coro/detail/signal_win32.hpp b/include/coro/detail/signal_win32.hpp index c2eef532..cd7c9e5b 100644 --- a/include/coro/detail/signal_win32.hpp +++ b/include/coro/detail/signal_win32.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include namespace coro::detail @@ -15,10 +16,8 @@ class signal_win32 void set(); void unset(); - private: - mutable void* m_iocp{}; - mutable void* m_data{}; - std::unique_ptr m_event; + mutable void* m_iocp{}; + mutable void* m_data{}; }; } // namespace coro::detail \ No newline at end of file diff --git a/include/coro/detail/winsock_handle.hpp b/include/coro/detail/winsock_handle.hpp new file mode 100644 index 00000000..76b7bf17 --- /dev/null +++ b/include/coro/detail/winsock_handle.hpp @@ -0,0 +1,42 @@ +#pragma once +#include +#include + +namespace coro::detail +{ +/** + * @brief RAII wrapper for WinSock (WSAStartup/WSACleanup) + * + * Ensures that WinSock is initialised once and is automatically cleaned up when the last user + * releases its reference. + * + * It is constructed via the `initialise_winsock()`. + * + * If it has been already cleaned up then future calls to `initialise_winsock()` + * will reinitialize WinSock. + */ +class winsock_handle +{ + struct private_constructor + { + }; + +public: + winsock_handle(private_constructor); + ~winsock_handle(); + + winsock_handle(const winsock_handle&) = delete; + auto operator=(const winsock_handle&) -> winsock_handle& = delete; + + winsock_handle(winsock_handle&&) = delete; + auto operator=(winsock_handle&&) -> winsock_handle& = delete; + + friend auto initialise_winsock() -> std::shared_ptr; + +private: + static inline std::mutex mutex; + static inline std::weak_ptr current_winsock_handle; +}; + +auto initialise_winsock() -> std::shared_ptr; +} \ No newline at end of file diff --git a/include/coro/fd.hpp b/include/coro/fd.hpp index aa91712e..bae022f6 100644 --- a/include/coro/fd.hpp +++ b/include/coro/fd.hpp @@ -1,7 +1,13 @@ #pragma once +#include "platform.hpp" +#include namespace coro { +#if defined(CORO_PLATFORM_UNIX) using fd_t = int; +#elif defined(CORO_PLATFORM_WINDOWS) +using fd_t = uint64_t *; +#endif } // namespace coro diff --git a/include/coro/io_scheduler.hpp b/include/coro/io_scheduler.hpp index b4e684f8..65a2adae 100644 --- a/include/coro/io_scheduler.hpp +++ b/include/coro/io_scheduler.hpp @@ -17,13 +17,12 @@ #include "coro/net/socket.hpp" #endif -#include "signal.hpp" +#include "coro/signal.hpp" #include #include #include #include -#include #include #include #include diff --git a/include/coro/net/ip_address.hpp b/include/coro/net/ip_address.hpp index 006769c0..1dfa8ee1 100644 --- a/include/coro/net/ip_address.hpp +++ b/include/coro/net/ip_address.hpp @@ -2,26 +2,21 @@ #include #include -#include #include #include #include -#include "coro/platform.hpp" - -#if defined(CORO_PLATFORM_UNIX) - #include -#elif defined(CORO_PLATFORM_WINDOWS) - #include -#endif namespace coro::net { +// TODO: convert to OS AF_INET, AF_INET6 enum class domain_t : int { - ipv4 = AF_INET, - ipv6 = AF_INET6 + ipv4, + ipv6 }; +auto domain_to_os(domain_t domain) -> int; + auto to_string(domain_t domain) -> const std::string&; class ip_address @@ -63,45 +58,9 @@ class ip_address } } - static auto from_string(const std::string& address, domain_t domain = domain_t::ipv4) -> ip_address - { - ip_address addr{}; - addr.m_domain = domain; - - auto success = inet_pton(static_cast(addr.m_domain), address.data(), addr.m_data.data()); - if (success != 1) - { - throw std::runtime_error{"coro::net::ip_address faild to convert from string"}; - } - - return addr; - } + static auto from_string(const std::string& address, domain_t domain = domain_t::ipv4) -> ip_address; - auto to_string() const -> std::string - { - std::string output; - if (m_domain == domain_t::ipv4) - { - output.resize(INET_ADDRSTRLEN, '\0'); - } - else - { - output.resize(INET6_ADDRSTRLEN, '\0'); - } - - auto success = inet_ntop(static_cast(m_domain), m_data.data(), output.data(), output.length()); - if (success != nullptr) - { - auto len = strnlen(success, output.length()); - output.resize(len); - } - else - { - throw std::runtime_error{"coro::net::ip_address failed to convert to string representation"}; - } - - return output; - } + auto to_string() const -> std::string; auto operator<=>(const ip_address& other) const = default; diff --git a/include/coro/net/socket.hpp b/include/coro/net/socket.hpp index 58579919..a712960a 100644 --- a/include/coro/net/socket.hpp +++ b/include/coro/net/socket.hpp @@ -1,8 +1,8 @@ #pragma once #include "coro/net/ip_address.hpp" -#include "coro/poll.hpp" #include "coro/platform.hpp" +#include "coro/poll.hpp" #include #include @@ -11,6 +11,8 @@ #if defined(CORO_PLATFORM_UNIX) #include #include +#elif defined(CORO_PLATFORM_WINDOWS) + #include #endif #include @@ -21,11 +23,11 @@ class socket { public: #if defined(CORO_PLATFORM_UNIX) - using native_handle_t = int; + using native_handle_t = int; constexpr static native_handle_t invalid_handle = -1; #elif defined(CORO_PLATFORM_WINDOWS) - using native_handle_t = unsigned int; - constexpr static native_handle_t invalid_handle = ~0u; // ~0 = -1, but for unsigned + using native_handle_t = void*; + constexpr static native_handle_t invalid_handle = reinterpret_cast(~0ull); // ~0 = -1, but for unsigned #endif enum class type_t @@ -57,18 +59,17 @@ class socket static auto type_to_os(type_t type) -> int; socket() = default; - explicit socket(int fd) : m_fd(fd) {} + explicit socket(native_handle_t fd) : m_fd(fd) {} - #if defined(CORO_PLATFORM_UNIX) socket(const socket& other) : m_fd(dup(other.m_fd)) {} auto operator=(const socket& other) noexcept -> socket&; #elif defined(CORO_PLATFORM_WINDOWS) - socket(const socket& other) = delete; + socket(const socket& other) = delete; auto operator=(const socket& other) noexcept = delete; #endif - socket(socket&& other) : m_fd(std::exchange(other.m_fd, -1)) {} + socket(socket&& other) noexcept : m_fd(std::exchange(other.m_fd, invalid_handle)) {} auto operator=(socket&& other) noexcept -> socket&; ~socket() { close(); } @@ -78,7 +79,7 @@ class socket * not imply if the socket is still usable. * @return True if the socket file descriptor is > 0. */ - auto is_valid() const -> bool { return m_fd != -1; } + auto is_valid() const -> bool { return m_fd != invalid_handle; } /** * @param block Sets the socket to the given blocking mode. @@ -103,6 +104,9 @@ class socket private: native_handle_t m_fd{invalid_handle}; +#if defined(CORO_PLATFORM_WINDOWS) + std::shared_ptr m_winsock = detail::initialise_winsock(); +#endif }; /** diff --git a/include/coro/net/tcp/client.hpp b/include/coro/net/tcp/client.hpp index 17499c95..f548efb3 100644 --- a/include/coro/net/tcp/client.hpp +++ b/include/coro/net/tcp/client.hpp @@ -147,7 +147,7 @@ class client /// Options for what server to connect to. options m_options{}; /// The tcp socket. - net::socket m_socket{-1}; + net::socket m_socket{}; /// Cache the status of the connect in the event the user calls connect() again. std::optional m_connect_status{std::nullopt}; }; diff --git a/include/coro/net/tcp/server.hpp b/include/coro/net/tcp/server.hpp index 1ada9912..79e87c14 100644 --- a/include/coro/net/tcp/server.hpp +++ b/include/coro/net/tcp/server.hpp @@ -46,11 +46,13 @@ class server auto operator=(server&& other) -> server&; ~server() = default; +#if defined(CORO_PLATFORM_UNIX) /** * Polls for new incoming tcp connections. * @param timeout How long to wait for a new connection before timing out, zero waits indefinitely. * @return The result of the poll, 'event' means the poll was successful and there is at least 1 * connection ready to be accepted. + * @note Unix only */ auto poll(std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task { @@ -61,8 +63,19 @@ class server * Accepts an incoming tcp client connection. On failure the tls clients socket will be set to * and invalid state, use the socket.is_value() to verify the client was correctly accepted. * @return The newly connected tcp client connection. + * @note Unix only */ auto accept() -> coro::net::tcp::client; +#endif + + /** + * Asynchronously accepts an incoming TCP client connection. + * If no connection is received before the internal timeout or cancellation, the result will be std::nullopt. + * + * @return A task resolving to an optional TCP client connection. The value will be set if a client was + * successfully accepted, or std::nullopt if the operation timed out or was cancelled. + */ + auto accept_client(std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task>; private: friend client; @@ -71,7 +84,7 @@ class server /// The bind and listen options for this server. options m_options; /// The socket for accepting new tcp connections on. - net::socket m_accept_socket{-1}; + net::socket m_accept_socket{}; }; } // namespace coro::net::tcp diff --git a/include/coro/poll.hpp b/include/coro/poll.hpp index 4764be7c..6bed0e6f 100644 --- a/include/coro/poll.hpp +++ b/include/coro/poll.hpp @@ -1,16 +1,13 @@ #pragma once -#include #include "platform.hpp" +#include #if defined(CORO_PLATFORM_LINUX) #include #endif #if defined(CORO_PLATFORM_BSD) #include #endif -#if defined(CORO_PLATFORM_WINDOWS) - #include -#endif namespace coro { @@ -36,14 +33,12 @@ enum class poll_op : int64_t read_write = -5 }; #elif defined(CORO_PLATFORM_WINDOWS) +// Windows doesn't have polling, so we don't use it. enum class poll_op : uint32_t { - /// Poll for read operations. - read = FD_READ, - /// Poll for write operations. - write = FD_WRITE, - /// Poll for read and write operations. - read_write = FD_READ | FD_WRITE + read, + write, + read_write }; #endif diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp index 7addf0f7..43f796fb 100644 --- a/src/detail/io_notifier_iocp.cpp +++ b/src/detail/io_notifier_iocp.cpp @@ -1,21 +1,14 @@ #include "coro/detail/io_notifier_iocp.hpp" -#include -#include #include "coro/detail/signal_win32.hpp" #include "coro/detail/timer_handle.hpp" +#include #include namespace coro::detail { -struct signal_win32::Event -{ - void* data; - bool is_set; -}; - io_notifier_iocp::io_notifier_iocp() { - std::size_t concurrent_threads = 0; // TODO + DWORD concurrent_threads = 0; // TODO m_iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, concurrent_threads); } @@ -28,17 +21,22 @@ io_notifier_iocp::~io_notifier_iocp() static VOID CALLBACK onTimerFired(LPVOID timerPtr, DWORD, DWORD) { auto* handle = static_cast(timerPtr); - + // Completion key 3 means timer - PostQueuedCompletionStatus(handle->get_iocp(), 0, 3, static_cast(const_cast(handle->get_inner()))); + PostQueuedCompletionStatus( + handle->get_iocp(), + 0, + static_cast(io_notifier::completion_key::timer), + static_cast(const_cast(handle->get_inner()))); } auto io_notifier_iocp::watch_timer(const detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool { - if (timer.m_iocp == nullptr) { + if (timer.m_iocp == nullptr) + { timer.m_iocp = m_iocp; } - else if (timer.m_iocp != m_iocp) + else if (timer.m_iocp != m_iocp) { throw std::runtime_error("Timer is already associated with a different IOCP handle. Cannot reassign."); } @@ -72,111 +70,140 @@ auto io_notifier_iocp::watch(const coro::signal& signal, void* data) -> bool } auto io_notifier_iocp::next_events( - std::vector>& ready_events, - const std::chrono::milliseconds timeout -) -> void + std::vector>& ready_events, + const std::chrono::milliseconds timeout, + const size_t max_events) -> void { - DWORD bytes_transferred = 0; - completion_key completion_key; - LPOVERLAPPED overlapped = nullptr; - DWORD timeoutMs = (timeout.count() == -1) ? INFINITE : static_cast(timeout.count()); - - process_active_signals(ready_events); - - while (true) - { - BOOL success = GetQueuedCompletionStatus( - m_iocp, &bytes_transferred, reinterpret_cast(std::addressof(completion_key)), &overlapped, timeoutMs); - - if (!success && overlapped == nullptr) - { - // Timeout or critical error - return; - } - - switch (completion_key) - { - case completion_key::signal: - { - if (!overlapped) - continue; - - auto* event = reinterpret_cast(overlapped); - set_signal_active(event->data, event->is_set); - if (event->is_set) - { - // poll_status doesn't matter. - ready_events.emplace_back(event->data, coro::poll_status::event); - } - continue; - } + using namespace std::chrono; + + // Лямбда разбора одного события + auto handle = [&](DWORD bytes, completion_key key, LPOVERLAPPED ov) { + switch (key) { + case completion_key::signal_set: + case completion_key::signal_unset: + if (ov) set_signal_active(reinterpret_cast(ov), + key == completion_key::signal_set); + break; case completion_key::socket: - { - auto* info = reinterpret_cast(overlapped); - if (!info) - continue; - - info->bytes_transferred = bytes_transferred; - - coro::poll_status status; - - if (!success) - { - DWORD err = GetLastError(); - if (err == ERROR_NETNAME_DELETED || err == ERROR_CONNECTION_ABORTED || - err == ERROR_OPERATION_ABORTED) - status = coro::poll_status::closed; - else - status = coro::poll_status::error; + if (ov) { + auto* info = reinterpret_cast(ov); + info->bytes_transferred = bytes; + coro::poll_status st = (bytes == 0 && !info->is_accept) + ? coro::poll_status::closed + : coro::poll_status::event; + ready_events.emplace_back(&info->pi, st); } - else if (bytes_transferred == 0) - { - // The connection is closed normally - status = coro::poll_status::closed; - } - else - { - status = coro::poll_status::event; - } - - ready_events.emplace_back(&info->pi, status); - continue; - } - case completion_key::timer: // timer - { - // Remember that it's not real poll_info, it's just a pointer to some random data - // io_scheduler must handle it. - // poll_status doesn't matter. - auto* handle_ptr = reinterpret_cast(overlapped); - ready_events.emplace_back(handle_ptr, coro::poll_status::event); break; - } + case completion_key::timer: + if (ov) + ready_events.emplace_back( + reinterpret_cast(ov), + coro::poll_status::event); + break; default: - { - throw std::runtime_error("Received unknown completion key."); + throw std::runtime_error("Unknown completion key"); + } + }; + + // Сначала сигналы + process_active_signals(ready_events); + + // --- 1) Обработка с тайм‑аутом >= 0 + if (timeout.count() >= 0) { + milliseconds remaining = timeout; + while (remaining.count() > 0 && ready_events.size() < max_events) { + // Засекаем время + auto t0 = steady_clock::now(); + DWORD bytes = 0; + completion_key key{}; + LPOVERLAPPED ov = nullptr; + + BOOL ok = GetQueuedCompletionStatus( + m_iocp, &bytes, + reinterpret_cast(&key), + &ov, + static_cast(remaining.count()) + ); + auto t1 = steady_clock::now(); + + // Тайм‑аут без событий — выходим в drain + if (!ok && ov == nullptr) { + break; } + + // Обрабатываем найденное событие + handle(bytes, key, ov); + + // Вычитаем потраченное время + auto took = duration_cast(t1 - t0); + if (took < remaining) + remaining -= took; + else + break; // время вышло + } + } + // --- 2) Или первый бесконечный wait, если timeout < 0 + else { + DWORD bytes = 0; + completion_key key{}; + LPOVERLAPPED ov = nullptr; + BOOL ok = GetQueuedCompletionStatus( + m_iocp, &bytes, + reinterpret_cast(&key), + &ov, + INFINITE + ); + if (ok || ov) { + handle(bytes, key, ov); } + // Любая ошибка без ov — просто выходим + if (!ok && ov == nullptr) { + return; + } + } + // --- 3) Дренч‑цикл: вычитываем всё доступное без ожидания + while (ready_events.size() < max_events) { + DWORD bytes = 0; + completion_key key{}; + LPOVERLAPPED ov = nullptr; + BOOL ok = GetQueuedCompletionStatus( + m_iocp, &bytes, + reinterpret_cast(&key), + &ov, + 0 // non-blocking + ); + if (!ok && ov == nullptr) + break; + handle(bytes, key, ov); } } + + + + + void io_notifier_iocp::set_signal_active(void* data, bool active) { std::scoped_lock lk{m_active_signals_mutex}; - if (active) { + if (active) + { m_active_signals.emplace_back(data); } - else { - m_active_signals.erase(std::remove(std::begin(m_active_signals), std::end(m_active_signals), data)); + else if (auto it = std::find(m_active_signals.begin(), m_active_signals.end(), data); it != m_active_signals.end()) + { + // Fast erase + std::swap(m_active_signals.back(), *it); + m_active_signals.pop_back(); } } void io_notifier_iocp::process_active_signals( - std::vector>& ready_events -) + std::vector>& ready_events) { for (void* data : m_active_signals) { // poll_status doesn't matter. - ready_events.emplace_back(data, poll_status::event); + ready_events.emplace_back(static_cast(data), poll_status::event); } } -} \ No newline at end of file +} // namespace coro::detail \ No newline at end of file diff --git a/src/detail/signal_win32.cpp b/src/detail/signal_win32.cpp index 42750128..40959146 100644 --- a/src/detail/signal_win32.cpp +++ b/src/detail/signal_win32.cpp @@ -4,14 +4,8 @@ namespace coro::detail { -// Maybe not thread-safe -struct signal_win32::Event -{ - void* data; - bool is_set; -}; -signal_win32::signal_win32() : m_event(std::make_unique()) +signal_win32::signal_win32() { } @@ -20,24 +14,22 @@ signal_win32::~signal_win32() } void signal_win32::set() { - m_event->is_set = true; - m_event->data = m_data; + printf("Set signal %p\n", m_data); PostQueuedCompletionStatus( m_iocp, 0, - (ULONG_PTR)io_notifier::completion_key::signal, - (LPOVERLAPPED)(void*)m_event.get() + static_cast(io_notifier::completion_key::signal_set), + (LPOVERLAPPED)m_data ); } void signal_win32::unset() { - m_event->is_set = false; - m_event->data = m_data; + printf("Unset signal %p\n", m_data); PostQueuedCompletionStatus( m_iocp, 0, - (ULONG_PTR)io_notifier::completion_key::signal, - (LPOVERLAPPED)(void*)m_event.get() + static_cast(io_notifier::completion_key::signal_unset), + (LPOVERLAPPED)m_data ); } } \ No newline at end of file diff --git a/src/detail/winsock_handle.cpp b/src/detail/winsock_handle.cpp new file mode 100644 index 00000000..3b500e7a --- /dev/null +++ b/src/detail/winsock_handle.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include + +#pragma comment(lib, "Ws2_32.lib") +#pragma comment(lib, "wsock32.lib") + +namespace coro::detail +{ +coro::detail::winsock_handle::winsock_handle(private_constructor) +{ + WSADATA data; + int r = WSAStartup(MAKEWORD(2, 2), &data); + if (r != 0) + { + throw std::runtime_error{"WSAStartup failed: " + std::to_string(r)}; + } +} + +coro::detail::winsock_handle::~winsock_handle() +{ + WSACleanup(); +} + +auto initialise_winsock() -> std::shared_ptr +{ + std::unique_lock lk{winsock_handle::mutex}; + + std::shared_ptr handle = winsock_handle::current_winsock_handle.lock(); + if (!handle) + { + handle = std::make_shared(winsock_handle::private_constructor{}); + winsock_handle::current_winsock_handle = handle; + } + + return handle; +} +} \ No newline at end of file diff --git a/src/io_scheduler.cpp b/src/io_scheduler.cpp index 20ea21f2..136f9f17 100644 --- a/src/io_scheduler.cpp +++ b/src/io_scheduler.cpp @@ -10,6 +10,8 @@ #if defined(CORO_PLATFORM_UNIX) #include #include +#elif defined(CORO_PLATFORM_WINDOWS) + #include #endif using namespace std::chrono_literals; @@ -141,8 +143,8 @@ auto io_scheduler::poll(detail::poll_info& pi, std::chrono::milliseconds timeout { m_size.fetch_add(1, std::memory_order::release); bool timeout_requested = (timeout > 0ms); - - if (timeout_requested) + + if (timeout_requested) { pi.m_timer_pos = add_timer_token(clock::now() + timeout, pi); } @@ -154,13 +156,13 @@ auto io_scheduler::poll(detail::poll_info& pi, std::chrono::milliseconds timeout } auto io_scheduler::bind_socket(const net::socket& sock) -> void { - std::size_t concurrent_threads = m_thread_pool ? m_thread_pool->size() : 0; + int concurrent_threads = m_thread_pool ? m_thread_pool->size() : 0; + HANDLE handle = CreateIoCompletionPort( - reinterpret_cast(sock.native_handle()), + (HANDLE)(sock.native_handle()), m_io_notifier.iocp(), static_cast(io_notifier::completion_key::socket), - concurrent_threads - ); + concurrent_threads); // TODO: check handle } #endif @@ -327,11 +329,13 @@ auto io_scheduler::process_event_execute(detail::poll_info* pi, poll_status stat // is ever processed, the other is discarded. pi->m_processed = true; +#if defined(CORO_PLATFORM_UNIX) // Given a valid fd always remove it from epoll so the next poll can blindly EPOLL_CTL_ADD. if (pi->m_fd != -1) { m_io_notifier.unwatch(*pi); } +#endif // Since this event triggered, remove its corresponding timeout if it has one. if (pi->m_timer_pos.has_value()) @@ -382,11 +386,13 @@ auto io_scheduler::process_timeout_execute() -> void // is ever processed, the other is discarded. pi->m_processed = true; +#if defined(CORO_PLATFORM_UNIX) // Since this timed out, remove its corresponding event if it has one. if (pi->m_fd != -1) { m_io_notifier.unwatch(*pi); } +#endif while (pi->m_awaiting_coroutine == nullptr) { @@ -445,7 +451,7 @@ auto io_scheduler::update_timeout(time_point now) -> void if (!m_io_notifier.watch_timer(m_timer, amount)) { - std::cerr << "Failed to set timerfd errorno=[" << std::string{strerror(errno)} << "]."; + std::cerr << "Failed to set timer errorno=[" << std::string{strerror(errno)} << "]."; } } else diff --git a/src/net/ip_address.cpp b/src/net/ip_address.cpp index 78cbee79..cfd565ae 100644 --- a/src/net/ip_address.cpp +++ b/src/net/ip_address.cpp @@ -1,10 +1,31 @@ #include "coro/net/ip_address.hpp" +#include + +#if defined(CORO_PLATFORM_UNIX) + #include +#elif defined(CORO_PLATFORM_WINDOWS) + #include + #include + #include +#endif namespace coro::net { static std::string domain_ipv4{"ipv4"}; static std::string domain_ipv6{"ipv6"}; +auto domain_to_os(domain_t domain) -> int +{ + switch (domain) + { + case domain_t::ipv4: + return AF_INET; + case domain_t::ipv6: + return AF_INET6; + } + throw std::runtime_error{"coro::net::to_string(domain_t) unknown domain"}; +} + auto to_string(domain_t domain) -> const std::string& { switch (domain) @@ -17,4 +38,44 @@ auto to_string(domain_t domain) -> const std::string& throw std::runtime_error{"coro::net::to_string(domain_t) unknown domain"}; } +auto ip_address::from_string(const std::string& address, domain_t domain) -> ip_address +{ + ip_address addr{}; + addr.m_domain = domain; + + auto success = inet_pton(domain_to_os(addr.m_domain), address.data(), addr.m_data.data()); + if (success != 1) + { + throw std::runtime_error{"coro::net::ip_address faild to convert from string"}; + } + + return addr; +} + +auto ip_address::to_string() const -> std::string +{ + std::string output; + if (m_domain == domain_t::ipv4) + { + output.resize(INET_ADDRSTRLEN, '\0'); + } + else + { + output.resize(INET6_ADDRSTRLEN, '\0'); + } + + auto success = inet_ntop(domain_to_os(m_domain), m_data.data(), output.data(), output.length()); + if (success != nullptr) + { + auto len = strnlen(success, output.length()); + output.resize(len); + } + else + { + throw std::runtime_error{"coro::net::ip_address failed to convert to string representation"}; + } + + return output; +} + } // namespace coro::net diff --git a/src/net/socket.cpp b/src/net/socket.cpp index be4d7035..3b8f0164 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -1,7 +1,7 @@ #include "coro/net/socket.hpp" #if defined(CORO_PLATFORM_WINDOWS) - #include #include + #include #elif defined(CORO_PLATFORM_UNIX) #include #endif @@ -22,7 +22,6 @@ auto socket::type_to_os(type_t type) -> int } } - #if defined(CORO_PLATFORM_UNIX) auto socket::operator=(const socket& other) noexcept -> socket& { @@ -36,7 +35,7 @@ auto socket::operator=(socket&& other) noexcept -> socket& { if (std::addressof(other) != this) { - m_fd = std::exchange(other.m_fd, -1); + m_fd = std::exchange(other.m_fd, invalid_handle); } return *this; @@ -44,7 +43,7 @@ auto socket::operator=(socket&& other) noexcept -> socket& auto socket::blocking(blocking_t block) -> bool { - if (m_fd < 0) + if (!is_valid()) { return false; } @@ -62,13 +61,13 @@ auto socket::blocking(blocking_t block) -> bool return (fcntl(m_fd, F_SETFL, flags) == 0); #elif defined(CORO_PLATFORM_WINDOWS) u_long mode = (block == blocking_t::yes) ? 0 : 1; - return ioctlsocket(m_fd, FIONBIO, &mode) == 0; + return ioctlsocket((SOCKET)m_fd, FIONBIO, &mode) == 0; #endif } auto socket::shutdown(poll_op how) -> bool { - if (m_fd == -1) + if (!is_valid()) { return false; } @@ -109,13 +108,12 @@ auto socket::shutdown(poll_op how) -> bool auto socket::close() -> void { - if (m_fd != -1) + if (is_valid()) { - #if defined(CORO_PLATFORM_UNIX) ::close(m_fd); #elif defined(CORO_PLATFORM_WINDOWS) - ::closesocket(m_fd); + ::closesocket((SOCKET)m_fd); #endif m_fd = socket::invalid_handle; } @@ -124,14 +122,16 @@ auto socket::close() -> void auto make_socket(const socket::options& opts) -> socket { #if defined(CORO_PLATFORM_UNIX) - socket s{::socket(static_cast(opts.domain), socket::type_to_os(opts.type), 0)}; - if (s.native_handle() != socket::invalid_handle) + socket s{::socket(domain_to_os(opts.domain), socket::type_to_os(opts.type), 0)}; + if (!s.is_valid()) { throw std::runtime_error{"Failed to create socket."}; } #elif defined(CORO_PLATFORM_WINDOWS) - socket s{::WSASocketA(static_cast(opts.domain), socket::type_to_os(opts.type), 0, NULL, 0, WSA_FLAG_OVERLAPPED)}; - if (s.native_handle() != INVALID_SOCKET) + auto winsock = detail::initialise_winsock(); + socket s{reinterpret_cast(::WSASocketW( + domain_to_os(opts.domain), socket::type_to_os(opts.type), 0, nullptr, 0, WSA_FLAG_OVERLAPPED))}; + if (!s.is_valid()) { throw std::runtime_error{"Failed to create socket."}; } @@ -163,28 +163,28 @@ auto make_accept_socket(const socket::options& opts, const net::ip_address& addr int sock_opt_name = SO_REUSEPORT; int* sock_opt_ptr = &sock_opt; #elif defined(CORO_PLATFORM_WINDOWS) - int sock_opt_name = SO_REUSEADDR; - const char *sock_opt_ptr = reinterpret_cast(&sock_opt); + int sock_opt_name = SO_REUSEADDR; + const char* sock_opt_ptr = reinterpret_cast(&sock_opt); #endif - if (setsockopt(s.native_handle(), SOL_SOCKET, sock_opt_name, sock_opt_ptr, sizeof(sock_opt)) < 0) + if (setsockopt((SOCKET)s.native_handle(), SOL_SOCKET, sock_opt_name, sock_opt_ptr, sizeof(sock_opt)) < 0) { throw std::runtime_error{"Failed to setsockopt."}; } sockaddr_in server{}; - server.sin_family = static_cast(opts.domain); + server.sin_family = domain_to_os(opts.domain); server.sin_port = htons(port); server.sin_addr = *reinterpret_cast(address.data().data()); - if (bind(s.native_handle(), reinterpret_cast(&server), sizeof(server)) < 0) + if (bind((SOCKET)s.native_handle(), reinterpret_cast(&server), sizeof(server)) < 0) { throw std::runtime_error{"Failed to bind."}; } if (opts.type == socket::type_t::tcp) { - if (listen(s.native_handle(), backlog) < 0) + if (listen((SOCKET)s.native_handle(), backlog) < 0) { throw std::runtime_error{"Failed to listen."}; } diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index b3186358..7d57aaf5 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -1,5 +1,9 @@ #include "coro/net/tcp/client.hpp" +// The order of includes matters +// clang-format off +#include #include +// clang-format on #include namespace coro::net::tcp @@ -60,7 +64,6 @@ auto client::operator=(client&& other) noexcept -> client& return *this; } - #if defined(CORO_PLATFORM_UNIX) client::client(const client& other) : m_io_scheduler(other.m_io_scheduler), @@ -100,7 +103,7 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task(m_options.address.domain()); + server.sin_family = domain_to_os(m_options.address.domain()); server.sin_port = htons(m_options.port); server.sin_addr = *reinterpret_cast(m_options.address.data().data()); @@ -140,13 +143,18 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task(m_socket.native_handle()), SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), @@ -155,28 +163,27 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task(&server), - sizeof(server), - nullptr, - 0, + reinterpret_cast(m_socket.native_handle()), + reinterpret_cast(&server), + sizeof(server), + nullptr, + 0, 0, - &ovpi.ov - ); + &ovpi.ov); - if (res) + if (res) { - setsockopt(m_socket.native_handle(), SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0); + setsockopt( + reinterpret_cast(m_socket.native_handle()), SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0); co_return return_value(connect_status::connected); } - if (WSAGetLastError() == WSA_IO_PENDING) + if (WSAGetLastError() == WSA_IO_PENDING) { auto status = co_await m_io_scheduler->poll(ovpi.pi, timeout); if (status == poll_status::event) @@ -205,12 +212,12 @@ auto client::write(std::span buffer, std::chrono::milliseconds timeo -> task>> { detail::overlapped_io_operation ov{}; - WSABUF buf; + WSABUF buf; buf.buf = const_cast(buffer.data()); buf.len = buffer.size(); DWORD flags = 0, bytes_sent = 0; - int r = WSASend(m_socket.native_handle(), &buf, 1, &bytes_sent, flags, &ov.ov, nullptr); + int r = WSASend(reinterpret_cast(m_socket.native_handle()), &buf, 1, &bytes_sent, flags, &ov.ov, nullptr); if (r == 0) // Data already sent { if (bytes_sent == 0) @@ -227,12 +234,11 @@ auto client::write(std::span buffer, std::chrono::milliseconds timeo } else if (status == poll_status::timeout) { - CancelIoEx((HANDLE)m_socket.native_handle(), &ov.ov); + CancelIoEx(static_cast(m_socket.native_handle()), &ov.ov); co_return {write_status::timeout, buffer}; } } - - + co_return {write_status::error, buffer}; } @@ -240,12 +246,12 @@ auto client::read(std::span buffer, std::chrono::milliseconds timeout) -> task>> { detail::overlapped_io_operation ov{}; - WSABUF buf; + WSABUF buf; buf.buf = buffer.data(); buf.len = buffer.size(); DWORD flags = 0, bytes_recv = 0; - int r = WSARecv(m_socket.native_handle(), &buf, 1, &bytes_recv, &flags, &ov.ov, nullptr); + int r = WSARecv(reinterpret_cast(m_socket.native_handle()), &buf, 1, &bytes_recv, &flags, &ov.ov, nullptr); if (r == 0) // Data already read { if (bytes_recv == 0) @@ -262,7 +268,7 @@ auto client::read(std::span buffer, std::chrono::milliseconds timeout) } else if (status == poll_status::timeout) { - CancelIoEx((HANDLE)m_socket.native_handle(), &ov.ov); + CancelIoEx(reinterpret_cast(m_socket.native_handle()), &ov.ov); co_return {read_status::timeout, std::span{}}; } } diff --git a/src/net/tcp/server.cpp b/src/net/tcp/server.cpp index a94590c6..f92b9d52 100644 --- a/src/net/tcp/server.cpp +++ b/src/net/tcp/server.cpp @@ -2,6 +2,16 @@ #include "coro/io_scheduler.hpp" +// The order of includes matters +// clang-format off +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include "coro/detail/iocp_overlapped.hpp" + +// clang-format on + namespace coro::net::tcp { server::server(std::shared_ptr scheduler, options opts) @@ -18,6 +28,10 @@ server::server(std::shared_ptr scheduler, options opts) { throw std::runtime_error{"tcp::server cannot have a nullptr io_scheduler"}; } +#if defined(CORO_PLATFORM_WINDOWS) + // Bind socket to IOCP + m_io_scheduler->bind_socket(m_accept_socket); +#endif } server::server(server&& other) @@ -38,14 +52,16 @@ auto server::operator=(server&& other) -> server& return *this; } +#if defined(CORO_PLATFORM_UNIX) auto server::accept() -> coro::net::tcp::client { sockaddr_in client{}; constexpr const int len = sizeof(struct sockaddr_in); - net::socket s{::accept( - m_accept_socket.native_handle(), + + net::socket s{reinterpret_cast(::accept( + reinterpret_cast(m_accept_socket.native_handle()), reinterpret_cast(&client), - const_cast(reinterpret_cast(&len)))}; + const_cast(reinterpret_cast(&len))))}; std::span ip_addr_view{ reinterpret_cast(&client.sin_addr.s_addr), @@ -61,4 +77,146 @@ auto server::accept() -> coro::net::tcp::client }}; }; +auto server::accept_client() -> coro::task> +{ +} +#elif defined(CORO_PLATFORM_WINDOWS) +auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::task> +{ + static LPFN_ACCEPTEX accept_ex_function; + static std::once_flag accept_ex_function_created; + + std::call_once( + accept_ex_function_created, + [this] + { + DWORD num_bytes{}; + GUID guid = WSAID_ACCEPTEX; + + int success = ::WSAIoctl( + reinterpret_cast(m_accept_socket.native_handle()), + SIO_GET_EXTENSION_FUNCTION_POINTER, + &guid, + sizeof(guid), + &accept_ex_function, + sizeof(accept_ex_function), + &num_bytes, + nullptr, + nullptr); + + if (success != 0 || !accept_ex_function) + throw std::runtime_error("Failed to retrieve AcceptEx function pointer"); + }); + + + static LPFN_GETACCEPTEXSOCKADDRS get_accept_ex_sock_addrs_function; + static std::once_flag get_accept_ex_sock_addrs_created; + std::call_once( + get_accept_ex_sock_addrs_created, + [this] + { + DWORD num_bytes{}; + GUID guid = WSAID_GETACCEPTEXSOCKADDRS; + + int success = ::WSAIoctl( + reinterpret_cast(m_accept_socket.native_handle()), + SIO_GET_EXTENSION_FUNCTION_POINTER, + &guid, + sizeof(guid), + &get_accept_ex_sock_addrs_function, + sizeof(get_accept_ex_sock_addrs_function), + &num_bytes, + nullptr, + nullptr); + + if (success != 0 || !get_accept_ex_sock_addrs_function) + throw std::runtime_error("Failed to retrieve GetAcceptExSockaddrs function pointer"); + }); + + detail::overlapped_io_operation ovpi{.is_accept = true}; + + auto client = net::make_socket( + socket::options{ + .domain = m_options.address.domain(), .type = socket::type_t::tcp, .blocking = socket::blocking_t::no}); + + // AcceptEx requires a buffer for local + remote address, also extra 32 bytes because MS recommends so + char accept_buffer[(sizeof(SOCKADDR_STORAGE) + 16) * 2] = {}; + + DWORD bytes_received = 0; + BOOL result = accept_ex_function( + reinterpret_cast(m_accept_socket.native_handle()), + reinterpret_cast(client.native_handle()), + accept_buffer, + 0, + sizeof(SOCKADDR_IN) + 16, + sizeof(SOCKADDR_IN) + 16, + &bytes_received, + &ovpi.ov); + + if (!result) + { + const DWORD err = ::WSAGetLastError(); + if (err != WSA_IO_PENDING) + { + co_return std::nullopt; + } + } + + auto status = result ? poll_status::event : (co_await m_io_scheduler->poll(ovpi.pi, timeout)); + + if (status == poll_status::event) + { + // 1) Обновляем контекст принятого сокета + SOCKET listen_handle = reinterpret_cast(m_accept_socket.native_handle()); + ::setsockopt( + reinterpret_cast(client.native_handle()), + SOL_SOCKET, + SO_UPDATE_ACCEPT_CONTEXT, + reinterpret_cast(&listen_handle), + sizeof(listen_handle)); + + // 2) Забираем адреса из accept_buffer + SOCKADDR* local_addr = nullptr; + SOCKADDR* remote_addr = nullptr; + int local_len = 0; + int remote_len = 0; + + get_accept_ex_sock_addrs_function( + accept_buffer, + 0, + sizeof(SOCKADDR_IN) + 16, + sizeof(SOCKADDR_IN) + 16, + &local_addr, + &local_len, + &remote_addr, + &remote_len); + + // 3) Распаковываем remote_addr → ip + порт + auto domain = remote_addr->sa_family == AF_INET ? domain_t::ipv4 : domain_t::ipv6; + net::ip_address address; + uint16_t port; + + if (domain == domain_t::ipv4) + { + auto* sin = reinterpret_cast(remote_addr); + address = net::ip_address{ + std::span{reinterpret_cast(&sin->sin_addr), sizeof(sin->sin_addr)}, domain_t::ipv4}; + port = ntohs(sin->sin_port); + } + else + { + auto* sin6 = reinterpret_cast(remote_addr); + address = net::ip_address{ + std::span{reinterpret_cast(&sin6->sin6_addr), sizeof(sin6->sin6_addr)}, domain_t::ipv6}; + port = ntohs(sin6->sin6_port); + } + + co_return coro::net::tcp::client{m_io_scheduler, std::move(client), client::options{address, port}}; + } + + co_return std::nullopt; +} + +#endif + } // namespace coro::net::tcp diff --git a/src/net/tls/client.cpp b/src/net/tls/client.cpp index 1c67fdf7..01020b91 100644 --- a/src/net/tls/client.cpp +++ b/src/net/tls/client.cpp @@ -100,7 +100,7 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task(m_options.address.domain()); + server.sin_family = domain_to_os(m_options.address.domain()); server.sin_port = htons(m_options.port); server.sin_addr = *reinterpret_cast(m_options.address.data().data()); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d3f573fa..3db159e4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,8 +35,8 @@ if(LIBCORO_FEATURE_NETWORKING) list(APPEND LIBCORO_TEST_SOURCE_FILES net/test_dns_resolver.cpp net/test_tcp_server.cpp - net/test_tls_server.cpp - net/test_udp_peers.cpp + # net/test_tls_server.cpp + # net/test_udp_peers.cpp ) endif() @@ -71,6 +71,8 @@ elseif(${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") $<$:-pipe> ) elseif(MSVC) + # Prevent Windows.h from defining min/max macros that conflict with names. + target_compile_definitions(${PROJECT_NAME} PUBLIC NOMINMAX) target_compile_options(${PROJECT_NAME} PRIVATE /W4) else() message(FATAL_ERROR "Unsupported compiler.") From 50a789b5a5b1990e78729397c04dd48b1c0095aa Mon Sep 17 00:00:00 2001 From: PyXiion Date: Wed, 9 Jul 2025 15:05:15 +0300 Subject: [PATCH 14/24] server::accept_client: fix timeout handling --- src/net/tcp/server.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/net/tcp/server.cpp b/src/net/tcp/server.cpp index f92b9d52..74d792be 100644 --- a/src/net/tcp/server.cpp +++ b/src/net/tcp/server.cpp @@ -108,9 +108,8 @@ auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::tas throw std::runtime_error("Failed to retrieve AcceptEx function pointer"); }); - - static LPFN_GETACCEPTEXSOCKADDRS get_accept_ex_sock_addrs_function; - static std::once_flag get_accept_ex_sock_addrs_created; + static LPFN_GETACCEPTEXSOCKADDRS get_accept_ex_sock_addrs_function; + static std::once_flag get_accept_ex_sock_addrs_created; std::call_once( get_accept_ex_sock_addrs_created, [this] @@ -166,7 +165,6 @@ auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::tas if (status == poll_status::event) { - // 1) Обновляем контекст принятого сокета SOCKET listen_handle = reinterpret_cast(m_accept_socket.native_handle()); ::setsockopt( reinterpret_cast(client.native_handle()), @@ -175,7 +173,6 @@ auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::tas reinterpret_cast(&listen_handle), sizeof(listen_handle)); - // 2) Забираем адреса из accept_buffer SOCKADDR* local_addr = nullptr; SOCKADDR* remote_addr = nullptr; int local_len = 0; @@ -191,7 +188,6 @@ auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::tas &remote_addr, &remote_len); - // 3) Распаковываем remote_addr → ip + порт auto domain = remote_addr->sa_family == AF_INET ? domain_t::ipv4 : domain_t::ipv6; net::ip_address address; uint16_t port; @@ -213,6 +209,11 @@ auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::tas co_return coro::net::tcp::client{m_io_scheduler, std::move(client), client::options{address, port}}; } + else if (status == poll_status::timeout) + { + CancelIoEx(reinterpret_cast(m_accept_socket.native_handle()), &ovpi.ov); + co_return std::nullopt; + } co_return std::nullopt; } From f2d94f400c3980ff3f02674bffd3507dd3995a66 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Wed, 9 Jul 2025 19:13:19 +0300 Subject: [PATCH 15/24] Fix sync requests with sockets --- src/net/socket.cpp | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/net/socket.cpp b/src/net/socket.cpp index 3b8f0164..1cf697cf 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -1,6 +1,8 @@ #include "coro/net/socket.hpp" + #if defined(CORO_PLATFORM_WINDOWS) #include + #include #include #elif defined(CORO_PLATFORM_UNIX) #include @@ -129,12 +131,26 @@ auto make_socket(const socket::options& opts) -> socket } #elif defined(CORO_PLATFORM_WINDOWS) auto winsock = detail::initialise_winsock(); - socket s{reinterpret_cast(::WSASocketW( - domain_to_os(opts.domain), socket::type_to_os(opts.type), 0, nullptr, 0, WSA_FLAG_OVERLAPPED))}; + socket s{reinterpret_cast( + ::WSASocketW(domain_to_os(opts.domain), socket::type_to_os(opts.type), 0, nullptr, 0, WSA_FLAG_OVERLAPPED))}; if (!s.is_valid()) { throw std::runtime_error{"Failed to create socket."}; } + + // FILE_SKIP_COMPLETION_PORT_ON_SUCCESS: + // Prevents completion packets from being queued to the IOCP if the operation completes synchronously, + // reducing unnecessary overhead for fast operations. + // FILE_SKIP_SET_EVENT_ON_HANDLE: + // Prevents the system from setting the event in OVERLAPPED.hEvent upon operation completion, + // which is unnecessary when using IOCP and can improve performance by avoiding extra kernel event signals. + const BOOL success = SetFileCompletionNotificationModes( + reinterpret_cast(s.native_handle()), + FILE_SKIP_COMPLETION_PORT_ON_SUCCESS | FILE_SKIP_SET_EVENT_ON_HANDLE); + if (!success) + { + throw std::runtime_error{"SetFileCompletionNotificationModes failed."}; + } #endif if (opts.blocking == socket::blocking_t::no) @@ -172,19 +188,18 @@ auto make_accept_socket(const socket::options& opts, const net::ip_address& addr throw std::runtime_error{"Failed to setsockopt."}; } - sockaddr_in server{}; - server.sin_family = domain_to_os(opts.domain); - server.sin_port = htons(port); - server.sin_addr = *reinterpret_cast(address.data().data()); + sockaddr_storage server{}; + std::size_t server_len{}; + address.to_os(port, server, server_len); - if (bind((SOCKET)s.native_handle(), reinterpret_cast(&server), sizeof(server)) < 0) + if (bind(reinterpret_cast(s.native_handle()), reinterpret_cast(&server), server_len) < 0) { throw std::runtime_error{"Failed to bind."}; } if (opts.type == socket::type_t::tcp) { - if (listen((SOCKET)s.native_handle(), backlog) < 0) + if (listen(reinterpret_cast(s.native_handle()), backlog) < 0) { throw std::runtime_error{"Failed to listen."}; } From 46116b92c4466f141e707e21d5c7c5f90d06932c Mon Sep 17 00:00:00 2001 From: PyXiion Date: Wed, 9 Jul 2025 19:14:48 +0300 Subject: [PATCH 16/24] udp::peer: write_to, read_from cross-platform methods --- include/coro/net/udp/peer.hpp | 203 +++++++++++++++++++++++++--------- src/net/udp/peer.cpp | 170 +++++++++++++++++++++++++++- 2 files changed, 315 insertions(+), 58 deletions(-) diff --git a/include/coro/net/udp/peer.hpp b/include/coro/net/udp/peer.hpp index 21018f91..1e824cae 100644 --- a/include/coro/net/udp/peer.hpp +++ b/include/coro/net/udp/peer.hpp @@ -3,9 +3,11 @@ #include "coro/concepts/buffer.hpp" #include "coro/io_scheduler.hpp" #include "coro/net/ip_address.hpp" +#include "coro/net/read_status.hpp" #include "coro/net/recv_status.hpp" #include "coro/net/send_status.hpp" #include "coro/net/socket.hpp" +#include "coro/net/write_status.hpp" #include "coro/task.hpp" #include @@ -49,6 +51,7 @@ class peer auto operator=(peer&&) noexcept -> peer& = default; ~peer() = default; +#if defined(CORO_PLATFORM_UNIX) /** * @param op The poll operation to perform on the udp socket. Note that if this is a send only * udp socket (did not bind) then polling for read will not work. @@ -68,32 +71,7 @@ class peer * un-sent will correspond to bytes at the end of the given buffer. */ template - auto sendto(const info& peer_info, const buffer_type& buffer) -> std::pair> - { - if (buffer.empty()) - { - return {send_status::ok, std::span{}}; - } - - sockaddr_in peer{}; - peer.sin_family = static_cast(peer_info.address.domain()); - peer.sin_port = htons(peer_info.port); - peer.sin_addr = *reinterpret_cast(peer_info.address.data().data()); - - socklen_t peer_len{sizeof(peer)}; - - auto bytes_sent = ::sendto( - m_socket.native_handle(), buffer.data(), buffer.size(), 0, reinterpret_cast(&peer), peer_len); - - if (bytes_sent >= 0) - { - return {send_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; - } - else - { - return {static_cast(errno), std::span{}}; - } - } + auto sendto(const info& peer_info, const buffer_type& buffer) -> std::pair>; /** * @param buffer The buffer to receive data into. @@ -103,45 +81,162 @@ class peer * it might not fill the entire buffer. */ template - auto recvfrom(buffer_type&& buffer) -> std::tuple> - { - // The user must bind locally to be able to receive packets. - if (!m_bound) - { - return {recv_status::udp_not_bound, peer::info{}, std::span{}}; - } + auto recvfrom(buffer_type&& buffer) -> std::tuple>; - sockaddr_in peer{}; - socklen_t peer_len{sizeof(peer)}; + auto write_to( + const info& peer_info, + std::span buffer, + std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + -> coro::task>>; - auto bytes_read = ::recvfrom( - m_socket.native_handle(), buffer.data(), buffer.size(), 0, reinterpret_cast(&peer), &peer_len); + auto read_from(std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + -> coro::task>>; +#elif defined(CORO_PLATFORM_WINDOWS) - if (bytes_read < 0) - { - return {static_cast(errno), peer::info{}, std::span{}}; - } + auto write_to( + const info& peer_info, + std::span buffer, + std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + -> coro::task>>; - std::span ip_addr_view{ - reinterpret_cast(&peer.sin_addr.s_addr), - sizeof(peer.sin_addr.s_addr), - }; - - return { - recv_status::ok, - peer::info{ - .address = net::ip_address{ip_addr_view, static_cast(peer.sin_family)}, - .port = ntohs(peer.sin_port)}, - std::span{buffer.data(), static_cast(bytes_read)}}; - } + auto read_from(std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + -> coro::task>>; + +#endif private: /// The scheduler that will drive this udp client. std::shared_ptr m_io_scheduler; /// The udp socket. - net::socket m_socket{-1}; + net::socket m_socket{net::socket::invalid_handle}; /// Did the user request this udp socket is bound locally to receive packets? bool m_bound{false}; }; +#if defined(CORO_PLATFORM_UNIX) +template +auto peer::sendto(const info& peer_info, const buffer_type& buffer) -> std::pair> +{ + if (buffer.empty()) + { + return {send_status::ok, std::span{}}; + } + + sockaddr_in peer{}; + peer.sin_family = static_cast(peer_info.address.domain()); + peer.sin_port = htons(peer_info.port); + peer.sin_addr = *reinterpret_cast(peer_info.address.data().data()); + + socklen_t peer_len{sizeof(peer)}; + + auto bytes_sent = ::sendto( + m_socket.native_handle(), buffer.data(), buffer.size(), 0, reinterpret_cast(&peer), peer_len); + + if (bytes_sent >= 0) + { + return {send_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; + } + else + { + return {static_cast(errno), std::span{}}; + } +} +template +auto peer::recvfrom(buffer_type&& buffer) -> std::tuple> +{ + // The user must bind locally to be able to receive packets. + if (!m_bound) + { + return {recv_status::udp_not_bound, peer::info{}, std::span{}}; + } + + sockaddr_in peer{}; + socklen_t peer_len{sizeof(peer)}; + + auto bytes_read = ::recvfrom( + m_socket.native_handle(), buffer.data(), buffer.size(), 0, reinterpret_cast(&peer), &peer_len); + + if (bytes_read < 0) + { + return {static_cast(errno), peer::info{}, std::span{}}; + } + + std::span ip_addr_view{ + reinterpret_cast(&peer.sin_addr.s_addr), + sizeof(peer.sin_addr.s_addr), + }; + + return { + recv_status::ok, + peer::info{ + .address = net::ip_address{ip_addr_view, static_cast(peer.sin_family)}, + .port = ntohs(peer.sin_port)}, + std::span{buffer.data(), static_cast(bytes_read)}}; +} +auto peer::write_to( + const info& peer_info, + std::span buffer, + std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + -> coro::task>>; +{ + if (auto status = co_await poll(poll_op::write, timeout)) + { + switch (status) + { + case poll_status::closed: + co_return {write_status::closed, std::span{buffer.data(), buffer.size()}}; + ; + case poll_status::error: + co_return {write_status::error, std::span{buffer.data(), buffer.size()}}; + case poll_status::timeout: + co_return {write_status::timeout, std::span{buffer.data(), buffer.size()}}; + default: + throw std::runtime_error("Unknown poll_status value."); + } + } + switch (auto&& [status, span] = sendto(peer_info, std::forward(buffer)); status) + { + case send_status::ok: + co_return {write_status::ok, span}; + case send_status::closed: + co_return {write_status::closed, span}; + default: + co_return {write_status::error, span}; + } +} + +auto peer::read_from(std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) + -> coro::task>> +{ + if (!m_bound) + { + return {recv_status::udp_not_bound, peer::info{}, std::span{}}; + } + + if (auto status = co_await poll(poll_op::read, timeout); status != poll_status::event) + { + switch (status) + { + case poll_status::closed: + co_return {read_status::closed, std::span{}}; + case poll_status::error: + co_return {read_status::error, std::span{}}; + case poll_status::timeout: + co_return {read_status::timeout, std::span{}}; + default: + throw std::runtime_error("Unknown poll_status value."); + } + } + switch (auto&& [status, info, span] = recvfrom(std::forward(buffer)); status) + { + case recv_status::ok: + co_return {read_status::ok, std::move(info), span}; + case recv_status::closed: + co_return {read_status::closed, std::move(info), span}; + default: + co_return {read_status::error, std::move(info), span}; + } +} +#endif + } // namespace coro::net::udp diff --git a/src/net/udp/peer.cpp b/src/net/udp/peer.cpp index fe517262..7dbe56bd 100644 --- a/src/net/udp/peer.cpp +++ b/src/net/udp/peer.cpp @@ -1,21 +1,183 @@ #include "coro/net/udp/peer.hpp" +// The order of includes matters +// clang-format off +#include +#include +#include +#include "coro/detail/iocp_overlapped.hpp" +// clang-format on + namespace coro::net::udp { peer::peer(std::shared_ptr scheduler, net::domain_t domain) : m_io_scheduler(std::move(scheduler)), m_socket(net::make_socket(net::socket::options{domain, net::socket::type_t::udp, net::socket::blocking_t::no})) { +#if defined(CORO_PLATFORM_WINDOWS) + // Bind socket to IOCP + m_io_scheduler->bind_socket(m_socket); +#endif } peer::peer(std::shared_ptr scheduler, const info& bind_info) : m_io_scheduler(std::move(scheduler)), - m_socket(net::make_accept_socket( - net::socket::options{bind_info.address.domain(), net::socket::type_t::udp, net::socket::blocking_t::no}, - bind_info.address, - bind_info.port)), + m_socket( + net::make_accept_socket( + net::socket::options{bind_info.address.domain(), net::socket::type_t::udp, net::socket::blocking_t::no}, + bind_info.address, + bind_info.port)), m_bound(true) { +#if defined(CORO_PLATFORM_WINDOWS) + // Bind socket to IOCP + m_io_scheduler->bind_socket(m_socket); +#endif +} +auto peer::write_to(const info& peer_info, std::span buffer, std::chrono::milliseconds timeout) + -> coro::task>> +{ + if (buffer.empty()) + co_return {write_status::ok, std::span{}}; + + coro::detail::overlapped_io_operation ov{}; + WSABUF buf; + buf.buf = const_cast(buffer.data()); + buf.len = buffer.size(); + DWORD bytes_sent = 0; + + sockaddr_storage server{}; + std::size_t server_length{}; + peer_info.address.to_os(peer_info.port, server, server_length); + + int r = WSASendTo( + reinterpret_cast(m_socket.native_handle()), + &buf, + 1, + &bytes_sent, + 0, + reinterpret_cast(&server), + server_length, + &ov.ov, + nullptr); + + if (r == 0) + { + if (bytes_sent == 0) + co_return {write_status::closed, buffer}; + co_return {write_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; + } + else if (WSAGetLastError() == WSA_IO_PENDING) + { + auto status = co_await m_io_scheduler->poll(ov.pi, timeout); + if (status == poll_status::event) + { + co_return { + write_status::ok, + std::span{buffer.data() + ov.bytes_transferred, buffer.size() - ov.bytes_transferred}}; + } + else if (status == poll_status::timeout) + { + BOOL success = CancelIoEx(static_cast(m_socket.native_handle()), &ov.ov); + if (!success) + { + int err = GetLastError(); + if (err == ERROR_NOT_FOUND) + { + // Operation has been completed + co_return { + write_status::ok, + std::span{ + buffer.data() + ov.bytes_transferred, buffer.size() - ov.bytes_transferred}}; + } + } + co_return {write_status::timeout, buffer}; + } + } + + co_return {write_status::error, buffer}; +} +auto peer::read_from(std::span buffer, std::chrono::milliseconds timeout) + -> coro::task>> +{ + if (!m_bound) + { + co_return {read_status::udp_not_bound, peer::info{}, std::span{}}; + } + + detail::overlapped_io_operation ov{}; + WSABUF buf; + buf.buf = buffer.data(); + buf.len = buffer.size(); + DWORD flags = 0, bytes_recv = 0; + + sockaddr_storage remote{}; + socklen_t remote_len = sizeof(remote); + + int r = WSARecvFrom( + reinterpret_cast(m_socket.native_handle()), + &buf, + 1, + &bytes_recv, + &flags, + reinterpret_cast(&remote), + &remote_len, + &ov.ov, + nullptr); + + auto get_remote_info = [&remote]() -> peer::info + { + ip_address remote_ip; + std::uint16_t remote_port; + if (remote.ss_family == AF_INET) + { + auto& addr = reinterpret_cast(remote); + std::span ip_addr_view{ + reinterpret_cast(&addr.sin_addr.s_addr), sizeof(addr.sin_addr.s_addr)}; + remote_ip = ip_address{ip_addr_view, domain_t::ipv4}; + remote_port = ntohs(addr.sin_port); + } + else + { + auto& addr = reinterpret_cast(remote); + std::span ip_addr_view{reinterpret_cast(&addr.sin6_addr), sizeof(addr.sin6_addr)}; + remote_ip = ip_address{ip_addr_view, domain_t::ipv6}; + remote_port = ntohs(addr.sin6_port); + } + return peer::info{std::move(remote_ip), remote_port}; + }; + + if (r == 0) // Data already read + { + if (bytes_recv == 0) + co_return {read_status::closed, peer::info{}, buffer}; + co_return {read_status::ok, get_remote_info(), std::span{buffer.data(), bytes_recv}}; + } + else if (WSAGetLastError() == WSA_IO_PENDING) + { + auto status = co_await m_io_scheduler->poll(ov.pi, timeout); + if (status == poll_status::event) + { + co_return {read_status::ok, get_remote_info(), std::span{buffer.data(), ov.bytes_transferred}}; + } + else if (status == poll_status::timeout) + { + BOOL success = CancelIoEx(reinterpret_cast(m_socket.native_handle()), &ov.ov); + if (!success) + { + int err = GetLastError(); + if (err == ERROR_NOT_FOUND) + { + // Operation has been completed + co_return { + read_status::ok, get_remote_info(), std::span{buffer.data(), ov.bytes_transferred}}; + } + } + co_return {read_status::timeout, peer::info{}, std::span{}}; + } + } + + co_return {read_status::error, peer::info{}, std::span{}}; } } // namespace coro::net::udp From 8aa61141606bdcf76fbe1b2f4257f5a7af1aa64a Mon Sep 17 00:00:00 2001 From: PyXiion Date: Wed, 9 Jul 2025 19:26:16 +0300 Subject: [PATCH 17/24] Add UDP support for Windows - Add write_to/read_from coro methods with timeout support for both platforms: * Unix: using poll() + sendto/recvfrom * Windows: using IOCP overlapped IO with WSASendTo/WSARecvFrom - Add Windows-specific socket initialization with IOCP binding - Maintain existing sendto/recvfrom for Unix compatibility - Update socket handle type to use invalid_handle constant - Fix formatting - address conversion utilities --- CMakeLists.txt | 2 +- include/coro/concepts/executor.hpp | 12 +++ include/coro/coro.hpp | 2 +- include/coro/net/dns/resolver.hpp | 2 +- include/coro/net/ip_address.hpp | 6 ++ include/coro/net/read_status.hpp | 4 +- src/detail/io_notifier_iocp.cpp | 99 ++++++++++-------------- src/detail/signal_win32.cpp | 4 +- src/net/ip_address.cpp | 40 ++++++++++ src/net/tcp/client.cpp | 118 +++++++++++++++++++++-------- test/CMakeLists.txt | 96 +++++++++++------------ test/bench.cpp | 70 ++++++----------- test/main.cpp | 2 + test/net/test_tcp_server.cpp | 33 ++++---- test/net/test_udp_peers.cpp | 35 +++------ test/test_io_scheduler.cpp | 15 +++- 16 files changed, 307 insertions(+), 233 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b5c05d2c..d87c881e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,7 +158,7 @@ if(LIBCORO_FEATURE_NETWORKING) include/coro/net/socket.hpp src/net/socket.cpp include/coro/net/tcp/client.hpp src/net/tcp/client.cpp include/coro/net/tcp/server.hpp src/net/tcp/server.cpp - # include/coro/net/udp/peer.hpp src/net/udp/peer.cpp + include/coro/net/udp/peer.hpp src/net/udp/peer.cpp ) if(LIBCORO_FEATURE_TLS) diff --git a/include/coro/concepts/executor.hpp b/include/coro/concepts/executor.hpp index d57acf09..353fe5f8 100644 --- a/include/coro/concepts/executor.hpp +++ b/include/coro/concepts/executor.hpp @@ -3,9 +3,13 @@ #include "coro/concepts/awaitable.hpp" #include "coro/fd.hpp" #include "coro/task.hpp" +#include "coro/platform.hpp" #ifdef LIBCORO_FEATURE_NETWORKING #include "coro/poll.hpp" +#if defined(CORO_PLATFORM_WINDOWS) + #include "coro/detail/poll_info.hpp" +#endif #endif // #ifdef LIBCORO_FEATURE_NETWORKING #include @@ -30,11 +34,19 @@ concept executor = requires(executor_type e, std::coroutine_handle<> c) }; #ifdef LIBCORO_FEATURE_NETWORKING +#if defined(CORO_PLATFORM_UNIX) template concept io_executor = executor and requires(executor_type e, std::coroutine_handle<> c, fd_t fd, coro::poll_op op, std::chrono::milliseconds timeout) { { e.poll(fd, op, timeout) } -> std::same_as>; }; +#elif defined(CORO_PLATFORM_WINDOWS) +template +concept io_executor = executor and requires(executor_type e, coro::detail::poll_info pi, std::chrono::milliseconds timeout) +{ + { e.poll(pi, timeout) } -> std::same_as>; +}; +#endif #endif // #ifdef LIBCORO_FEATURE_NETWORKING // clang-format on diff --git a/include/coro/coro.hpp b/include/coro/coro.hpp index 3a5f3ab2..0dbcd1a0 100644 --- a/include/coro/coro.hpp +++ b/include/coro/coro.hpp @@ -26,7 +26,7 @@ #include "coro/net/recv_status.hpp" #include "coro/net/send_status.hpp" #include "coro/net/socket.hpp" -// #include "coro/net/udp/peer.hpp" + #include "coro/net/udp/peer.hpp" #endif #include "coro/condition_variable.hpp" diff --git a/include/coro/net/dns/resolver.hpp b/include/coro/net/dns/resolver.hpp index dd00ab88..0b17b1a9 100644 --- a/include/coro/net/dns/resolver.hpp +++ b/include/coro/net/dns/resolver.hpp @@ -206,7 +206,7 @@ class resolver std::vector> poll_tasks{}; for (size_t i = 0; i < new_sockets; ++i) { - auto fd = static_cast(ares_sockets[i]); + auto fd = reinterpret_cast(ares_sockets[i]); // If this socket is not currently actively polling, start polling! if (m_active_sockets.emplace(fd).second) diff --git a/include/coro/net/ip_address.hpp b/include/coro/net/ip_address.hpp index 1dfa8ee1..2dde563c 100644 --- a/include/coro/net/ip_address.hpp +++ b/include/coro/net/ip_address.hpp @@ -6,6 +6,8 @@ #include #include +struct sockaddr_storage; + namespace coro::net { // TODO: convert to OS AF_INET, AF_INET6 @@ -64,6 +66,10 @@ class ip_address auto operator<=>(const ip_address& other) const = default; + auto to_os(std::uint16_t port, sockaddr_storage& storage, std::size_t& len) const -> void; + + static auto get_any_address(domain_t domain) -> ip_address; + private: domain_t m_domain{domain_t::ipv4}; std::array m_data{}; diff --git a/include/coro/net/read_status.hpp b/include/coro/net/read_status.hpp index 7f4cccc4..05c3171d 100644 --- a/include/coro/net/read_status.hpp +++ b/include/coro/net/read_status.hpp @@ -7,6 +7,8 @@ enum class read_status ok, closed, timeout, - error + error, + + udp_not_bound }; } \ No newline at end of file diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp index 43f796fb..947988ed 100644 --- a/src/detail/io_notifier_iocp.cpp +++ b/src/detail/io_notifier_iocp.cpp @@ -72,106 +72,93 @@ auto io_notifier_iocp::watch(const coro::signal& signal, void* data) -> bool auto io_notifier_iocp::next_events( std::vector>& ready_events, const std::chrono::milliseconds timeout, - const size_t max_events) -> void + const size_t max_events) -> void { using namespace std::chrono; - // Лямбда разбора одного события - auto handle = [&](DWORD bytes, completion_key key, LPOVERLAPPED ov) { - switch (key) { + auto handle = [&](const DWORD bytes, const completion_key key, const LPOVERLAPPED ov) + { + switch (key) + { case completion_key::signal_set: case completion_key::signal_unset: - if (ov) set_signal_active(reinterpret_cast(ov), - key == completion_key::signal_set); + if (ov) + set_signal_active(ov, key == completion_key::signal_set); break; case completion_key::socket: - if (ov) { - auto* info = reinterpret_cast(ov); + if (ov) + { + auto* info = reinterpret_cast(ov); info->bytes_transferred = bytes; - coro::poll_status st = (bytes == 0 && !info->is_accept) - ? coro::poll_status::closed - : coro::poll_status::event; + coro::poll_status st = + (bytes == 0 && !info->is_accept) ? coro::poll_status::closed : coro::poll_status::event; ready_events.emplace_back(&info->pi, st); } break; case completion_key::timer: if (ov) - ready_events.emplace_back( - reinterpret_cast(ov), - coro::poll_status::event); + ready_events.emplace_back(reinterpret_cast(ov), coro::poll_status::event); break; default: throw std::runtime_error("Unknown completion key"); } }; - // Сначала сигналы process_active_signals(ready_events); - // --- 1) Обработка с тайм‑аутом >= 0 - if (timeout.count() >= 0) { + if (timeout.count() >= 0) + { milliseconds remaining = timeout; - while (remaining.count() > 0 && ready_events.size() < max_events) { - // Засекаем время - auto t0 = steady_clock::now(); - DWORD bytes = 0; + while (remaining.count() > 0 && ready_events.size() < max_events) + { + auto t0 = steady_clock::now(); + DWORD bytes = 0; completion_key key{}; - LPOVERLAPPED ov = nullptr; + LPOVERLAPPED ov = nullptr; BOOL ok = GetQueuedCompletionStatus( - m_iocp, &bytes, - reinterpret_cast(&key), - &ov, - static_cast(remaining.count()) - ); + m_iocp, &bytes, reinterpret_cast(&key), &ov, static_cast(remaining.count())); auto t1 = steady_clock::now(); - // Тайм‑аут без событий — выходим в drain - if (!ok && ov == nullptr) { + if (!ok && ov == nullptr) + { break; } - // Обрабатываем найденное событие handle(bytes, key, ov); - // Вычитаем потраченное время auto took = duration_cast(t1 - t0); if (took < remaining) remaining -= took; else - break; // время вышло + break; } } - // --- 2) Или первый бесконечный wait, если timeout < 0 - else { - DWORD bytes = 0; + else + { + DWORD bytes = 0; completion_key key{}; - LPOVERLAPPED ov = nullptr; - BOOL ok = GetQueuedCompletionStatus( - m_iocp, &bytes, - reinterpret_cast(&key), - &ov, - INFINITE - ); - if (ok || ov) { - handle(bytes, key, ov); - } - // Любая ошибка без ov — просто выходим - if (!ok && ov == nullptr) { + LPOVERLAPPED ov = nullptr; + BOOL ok = GetQueuedCompletionStatus(m_iocp, &bytes, reinterpret_cast(&key), &ov, INFINITE); + + if (!ok && ov == nullptr) + { return; } + handle(bytes, key, ov); } - // --- 3) Дренч‑цикл: вычитываем всё доступное без ожидания - while (ready_events.size() < max_events) { - DWORD bytes = 0; + while (ready_events.size() < max_events) + { + DWORD bytes = 0; completion_key key{}; - LPOVERLAPPED ov = nullptr; - BOOL ok = GetQueuedCompletionStatus( - m_iocp, &bytes, + LPOVERLAPPED ov = nullptr; + BOOL ok = GetQueuedCompletionStatus( + m_iocp, + &bytes, reinterpret_cast(&key), &ov, - 0 // non-blocking + 0 // non-blocking ); if (!ok && ov == nullptr) break; @@ -179,10 +166,6 @@ auto io_notifier_iocp::next_events( } } - - - - void io_notifier_iocp::set_signal_active(void* data, bool active) { std::scoped_lock lk{m_active_signals_mutex}; diff --git a/src/detail/signal_win32.cpp b/src/detail/signal_win32.cpp index 40959146..9833811b 100644 --- a/src/detail/signal_win32.cpp +++ b/src/detail/signal_win32.cpp @@ -1,6 +1,6 @@ +#include #include #include -#include namespace coro::detail { @@ -14,7 +14,6 @@ signal_win32::~signal_win32() } void signal_win32::set() { - printf("Set signal %p\n", m_data); PostQueuedCompletionStatus( m_iocp, 0, @@ -24,7 +23,6 @@ void signal_win32::set() } void signal_win32::unset() { - printf("Unset signal %p\n", m_data); PostQueuedCompletionStatus( m_iocp, 0, diff --git a/src/net/ip_address.cpp b/src/net/ip_address.cpp index cfd565ae..60dd7f9e 100644 --- a/src/net/ip_address.cpp +++ b/src/net/ip_address.cpp @@ -77,5 +77,45 @@ auto ip_address::to_string() const -> std::string return output; } +auto ip_address::to_os(const std::uint16_t port, sockaddr_storage& storage, std::size_t& len) const -> void +{ + switch (domain()) + { + case domain_t::ipv4: + { + auto& addr = reinterpret_cast(storage); + addr.sin_family = domain_to_os(domain()); + addr.sin_addr = *reinterpret_cast(data().data()); + addr.sin_port = htons(port); + len = sizeof(sockaddr_in); + return; + } + case domain_t::ipv6: + { + auto& addr = reinterpret_cast(storage); + addr.sin6_family = domain_to_os(domain()); + addr.sin6_addr = *reinterpret_cast(data().data()); + addr.sin6_port = htons(port); + addr.sin6_flowinfo = 0; + addr.sin6_scope_id = 0; + len = sizeof(sockaddr_in6); + return; + } + default: + throw std::runtime_error{"coro::net::ip_address unknown domain"}; + } +} +auto ip_address::get_any_address(domain_t domain) -> ip_address +{ + switch (domain) + { + case domain_t::ipv4: + return from_string("0.0.0.0", domain); + case domain_t::ipv6: + return from_string("::", domain); + default: + throw std::runtime_error{"coro::net::ip_address unknown domain"}; + } +} } // namespace coro::net diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index 7d57aaf5..513a0533 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -2,9 +2,10 @@ // The order of includes matters // clang-format off #include +#include #include -// clang-format on #include +// clang-format on namespace coro::net::tcp { @@ -38,6 +39,11 @@ client::client(std::shared_ptr scheduler, net::socket socket, opti // Force the socket to be non-blocking. m_socket.blocking(coro::net::socket::blocking_t::no); + +#if defined(CORO_PLATFORM_WINDOWS) + // Bind socket to IOCP + m_io_scheduler->bind_socket(m_socket); +#endif } client::client(client&& other) noexcept @@ -95,20 +101,19 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task connect_status { m_connect_status = s; return s; }; - sockaddr_in server{}; - server.sin_family = domain_to_os(m_options.address.domain()); - server.sin_port = htons(m_options.port); - server.sin_addr = *reinterpret_cast(m_options.address.data().data()); + sockaddr_storage server_storage{}; + std::size_t server_length{}; + m_options.address.to_os(m_options.port, server_storage, server_length); #if defined(CORO_PLATFORM_UNIX) - auto cret = ::connect(m_socket.native_handle(), reinterpret_cast(&server), sizeof(server)); + auto cret = ::connect(m_socket.native_handle(), reinterpret_cast(&server_storage), server_length); if (cret == 0) { co_return return_value(connect_status::connected); @@ -163,38 +168,70 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task(m_socket.native_handle()), + reinterpret_cast(&local_addr_storage), + local_addr_length) == SOCKET_ERROR) + { + co_return return_value(connect_status::error); + } + + // Now connect + BOOL result = connect_ex_function( reinterpret_cast(m_socket.native_handle()), - reinterpret_cast(&server), - sizeof(server), + reinterpret_cast(&server_storage), + server_length, nullptr, 0, - 0, + nullptr, &ovpi.ov); - if (res) + if (!result) { - setsockopt( - reinterpret_cast(m_socket.native_handle()), SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0); - co_return return_value(connect_status::connected); + const DWORD err = ::WSAGetLastError(); + if (err != WSA_IO_PENDING) + { + co_return return_value(connect_status::error); + } } - if (WSAGetLastError() == WSA_IO_PENDING) + auto status = result ? poll_status::event : co_await m_io_scheduler->poll(ovpi.pi, timeout); + + if (status == poll_status::event) { - auto status = co_await m_io_scheduler->poll(ovpi.pi, timeout); - if (status == poll_status::event) - { - co_return return_value(connect_status::connected); - } - else if (status == poll_status::timeout) + int error = 0; + int error_len = sizeof(error); + if (getsockopt( + reinterpret_cast(m_socket.native_handle()), + SOL_SOCKET, + SO_ERROR, + reinterpret_cast(&error), + &error_len) != 0 || + error != 0) { - CancelIoEx((HANDLE)m_socket.native_handle(), &ovpi.ov); - co_return return_value(connect_status::timeout); + co_return return_value(connect_status::error); } + setsockopt( + reinterpret_cast(m_socket.native_handle()), SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0); + co_return return_value(connect_status::connected); + } + else if (status == poll_status::timeout) + { + CancelIoEx((HANDLE)m_socket.native_handle(), &ovpi.ov); + co_return return_value(connect_status::timeout); } co_return return_value(connect_status::error); @@ -227,14 +264,27 @@ auto client::write(std::span buffer, std::chrono::milliseconds timeo else if (WSAGetLastError() == WSA_IO_PENDING) { auto status = co_await m_io_scheduler->poll(ov.pi, timeout); - bytes_sent = ov.bytes_transferred; if (status == poll_status::event) { - co_return {write_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; + co_return { + write_status::ok, + std::span{buffer.data() + ov.bytes_transferred, buffer.size() - ov.bytes_transferred}}; } else if (status == poll_status::timeout) { - CancelIoEx(static_cast(m_socket.native_handle()), &ov.ov); + BOOL success = CancelIoEx(static_cast(m_socket.native_handle()), &ov.ov); + if (!success) + { + int err = GetLastError(); + if (err == ERROR_NOT_FOUND) + { + // Operation has been completed + co_return { + write_status::ok, + std::span{ + buffer.data() + ov.bytes_transferred, buffer.size() - ov.bytes_transferred}}; + } + } co_return {write_status::timeout, buffer}; } } @@ -261,14 +311,22 @@ auto client::read(std::span buffer, std::chrono::milliseconds timeout) else if (WSAGetLastError() == WSA_IO_PENDING) { auto status = co_await m_io_scheduler->poll(ov.pi, timeout); - bytes_recv = ov.bytes_transferred; if (status == poll_status::event) { - co_return {read_status::ok, std::span{buffer.data(), bytes_recv}}; + co_return {read_status::ok, std::span{buffer.data(), ov.bytes_transferred}}; } else if (status == poll_status::timeout) { - CancelIoEx(reinterpret_cast(m_socket.native_handle()), &ov.ov); + BOOL success = CancelIoEx(reinterpret_cast(m_socket.native_handle()), &ov.ov); + if (!success) + { + int err = GetLastError(); + if (err == ERROR_NOT_FOUND) + { + // Operation has been completed + co_return {read_status::ok, std::span{buffer.data(), ov.bytes_transferred}}; + } + } co_return {read_status::timeout, std::span{}}; } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3db159e4..641675d1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,86 +2,86 @@ cmake_minimum_required(VERSION 3.12) project(libcoro_test) set(LIBCORO_TEST_SOURCE_FILES - test_condition_variable.cpp - test_event.cpp - test_generator.cpp - test_latch.cpp - test_mutex.cpp - test_ring_buffer.cpp - test_queue.cpp - test_semaphore.cpp - test_shared_mutex.cpp - test_sync_wait.cpp - test_task.cpp - test_thread_pool.cpp - test_when_all.cpp + test_condition_variable.cpp + test_event.cpp + test_generator.cpp + test_latch.cpp + test_mutex.cpp + test_ring_buffer.cpp + test_queue.cpp + test_semaphore.cpp + test_shared_mutex.cpp + test_sync_wait.cpp + test_task.cpp + test_thread_pool.cpp + test_when_all.cpp - catch_amalgamated.hpp catch_amalgamated.cpp - catch_extensions.hpp catch_extensions.cpp + catch_amalgamated.hpp catch_amalgamated.cpp + catch_extensions.hpp catch_extensions.cpp ) -if(NOT EMSCRIPTEN) +if (NOT EMSCRIPTEN) list(APPEND LIBCORO_TEST_SOURCE_FILES - test_when_any.cpp + test_when_any.cpp ) -endif() +endif () -if(LIBCORO_FEATURE_NETWORKING) +if (LIBCORO_FEATURE_NETWORKING) list(APPEND LIBCORO_TEST_SOURCE_FILES - net/test_ip_address.cpp + net/test_ip_address.cpp ) # These tests require coro::io_scheduler list(APPEND LIBCORO_TEST_SOURCE_FILES - net/test_dns_resolver.cpp - net/test_tcp_server.cpp - # net/test_tls_server.cpp - # net/test_udp_peers.cpp + # net/test_dns_resolver.cpp + net/test_tcp_server.cpp + # net/test_tls_server.cpp + net/test_udp_peers.cpp ) -endif() +endif () -if(LIBCORO_FEATURE_NETWORKING) +if (LIBCORO_FEATURE_NETWORKING) list(APPEND LIBCORO_TEST_SOURCE_FILES - bench.cpp - test_io_scheduler.cpp + bench.cpp + test_io_scheduler.cpp ) -endif() +endif () add_executable(${PROJECT_NAME} main.cpp ${LIBCORO_TEST_SOURCE_FILES}) target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(${PROJECT_NAME} PRIVATE libcoro) -if(${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") +if (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") target_compile_options(${PROJECT_NAME} PRIVATE - $<$:-std=c++20> - $<$:-fcoroutines> - $<$:-fconcepts> - $<$:-fexceptions> - $<$:-Wall> - $<$:-Wextra> - $<$:-pipe> + $<$:-std=c++20> + $<$:-fcoroutines> + $<$:-fconcepts> + $<$:-fexceptions> + $<$:-Wall> + $<$:-Wextra> + $<$:-pipe> ) -elseif(${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") +elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") target_compile_options(${PROJECT_NAME} PRIVATE - $<$:-std=c++20> - $<$:-fexceptions> - $<$:-Wall> - $<$:-Wextra> - $<$:-pipe> + $<$:-std=c++20> + $<$:-fexceptions> + $<$:-Wall> + $<$:-Wextra> + $<$:-pipe> ) -elseif(MSVC) +elseif (MSVC) # Prevent Windows.h from defining min/max macros that conflict with names. target_compile_definitions(${PROJECT_NAME} PUBLIC NOMINMAX) target_compile_options(${PROJECT_NAME} PRIVATE /W4) -else() +else () message(FATAL_ERROR "Unsupported compiler.") -endif() +endif () -if(LIBCORO_CODE_COVERAGE) +if (LIBCORO_CODE_COVERAGE) target_link_libraries(${PROJECT_NAME} PRIVATE gcov) target_compile_options(${PROJECT_NAME} PRIVATE --coverage) -endif() +endif () add_test(NAME libcoro_tests COMMAND ${PROJECT_NAME}) set_tests_properties(libcoro_tests PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<$:$>") diff --git a/test/bench.cpp b/test/bench.cpp index a08c43af..e264bd3c 100644 --- a/test/bench.cpp +++ b/test/bench.cpp @@ -403,21 +403,18 @@ TEST_CASE("benchmark tcp::server echo server thread pool", "[benchmark]") // Echo the messages until the socket is closed. while (true) { - auto pstatus = co_await client.poll(coro::poll_op::read); - REQUIRE_THREAD_SAFE(pstatus == coro::poll_status::event); - - auto [rstatus, rspan] = client.recv(in); - if (rstatus == coro::net::recv_status::closed) + auto [rstatus, rspan] = co_await client.read(in); + if (rstatus == coro::net::read_status::closed) { REQUIRE_THREAD_SAFE(rspan.empty()); break; } - REQUIRE_THREAD_SAFE(rstatus == coro::net::recv_status::ok); + REQUIRE_THREAD_SAFE(rstatus == coro::net::read_status::ok); in.resize(rspan.size()); - auto [sstatus, remaining] = client.send(in); - REQUIRE_THREAD_SAFE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await client.write(in); + REQUIRE_THREAD_SAFE(sstatus == coro::net::write_status::ok); REQUIRE_THREAD_SAFE(remaining.empty()); } @@ -435,15 +432,10 @@ TEST_CASE("benchmark tcp::server echo server thread pool", "[benchmark]") while (accepted.load(std::memory_order::acquire) < connections) { - auto pstatus = co_await server.poll(std::chrono::milliseconds{1}); - if (pstatus == coro::poll_status::event) + if (auto c = co_await server.accept_client(); c && c->socket().is_valid()) { - auto c = server.accept(); - if (c.socket().is_valid()) - { - accepted.fetch_add(1, std::memory_order::release); - server_scheduler->spawn(make_on_connection_task(std::move(c), wait_for_clients)); - } + accepted.fetch_add(1, std::memory_order::release); + server_scheduler->spawn(make_on_connection_task(std::move(*c), wait_for_clients)); } } @@ -476,16 +468,13 @@ TEST_CASE("benchmark tcp::server echo server thread pool", "[benchmark]") for (size_t i = 1; i <= messages_per_connection; ++i) { auto req_start = std::chrono::steady_clock::now(); - auto [sstatus, remaining] = client.send(msg); - REQUIRE_THREAD_SAFE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await client.write(msg); + REQUIRE_THREAD_SAFE(sstatus == coro::net::write_status::ok); REQUIRE_THREAD_SAFE(remaining.empty()); - auto pstatus = co_await client.poll(coro::poll_op::read); - REQUIRE_THREAD_SAFE(pstatus == coro::poll_status::event); - std::string response(64, '\0'); - auto [rstatus, rspan] = client.recv(response); - REQUIRE_THREAD_SAFE(rstatus == coro::net::recv_status::ok); + auto [rstatus, rspan] = co_await client.read(response); + REQUIRE_THREAD_SAFE(rstatus == coro::net::read_status::ok); REQUIRE_THREAD_SAFE(rspan.size() == msg.size()); response.resize(rspan.size()); REQUIRE_THREAD_SAFE(response == msg); @@ -596,21 +585,18 @@ TEST_CASE("benchmark tcp::server echo server inline", "[benchmark]") // Echo the messages until the socket is closed. while (true) { - auto pstatus = co_await client.poll(coro::poll_op::read); - REQUIRE_THREAD_SAFE(pstatus == coro::poll_status::event); - - auto [rstatus, rspan] = client.recv(in); - if (rstatus == coro::net::recv_status::closed) + auto [rstatus, rspan] = co_await client.read(in); + if (rstatus == coro::net::read_status::closed) { REQUIRE_THREAD_SAFE(rspan.empty()); break; } - REQUIRE_THREAD_SAFE(rstatus == coro::net::recv_status::ok); + REQUIRE_THREAD_SAFE(rstatus == coro::net::read_status::ok); in.resize(rspan.size()); - auto [sstatus, remaining] = client.send(in); - REQUIRE_THREAD_SAFE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await client.write(in); + REQUIRE_THREAD_SAFE(sstatus == coro::net::write_status::ok); REQUIRE_THREAD_SAFE(remaining.empty()); } @@ -633,15 +619,10 @@ TEST_CASE("benchmark tcp::server echo server inline", "[benchmark]") while (accepted_clients < connections_per_client) { - auto pstatus = co_await server.poll(std::chrono::milliseconds{1000}); - if (pstatus == coro::poll_status::event) + if (auto c = co_await server.accept_client(); c && c->socket().is_valid()) { - auto c = server.accept(); - if (c.socket().is_valid()) - { - s.live_clients++; - s.scheduler->spawn(make_on_connection_task(s, std::move(c))); - } + s.live_clients++; + s.scheduler->spawn(make_on_connection_task(s, std::move(*c))); } } @@ -685,16 +666,13 @@ TEST_CASE("benchmark tcp::server echo server inline", "[benchmark]") for (size_t i = 1; i <= messages_per_connection; ++i) { auto req_start = std::chrono::steady_clock::now(); - auto [sstatus, remaining] = client.send(msg); - REQUIRE_THREAD_SAFE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await client.write(msg); + REQUIRE_THREAD_SAFE(sstatus == coro::net::write_status::ok); REQUIRE_THREAD_SAFE(remaining.empty()); - auto pstatus = co_await client.poll(coro::poll_op::read); - REQUIRE_THREAD_SAFE(pstatus == coro::poll_status::event); - std::string response(64, '\0'); - auto [rstatus, rspan] = client.recv(response); - REQUIRE_THREAD_SAFE(rstatus == coro::net::recv_status::ok); + auto [rstatus, rspan] = co_await client.read(response); + REQUIRE_THREAD_SAFE(rstatus == coro::net::read_status::ok); REQUIRE_THREAD_SAFE(rspan.size() == msg.size()); response.resize(rspan.size()); REQUIRE_THREAD_SAFE(response == msg); diff --git a/test/main.cpp b/test/main.cpp index d6affd04..39441640 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -12,8 +12,10 @@ struct test_setup_networking { test_setup_networking() { + #if defined(CORO_PLATFORM_UNIX) // Ignore SIGPIPE, the library should be handling these gracefully. signal(SIGPIPE, SIG_IGN); + #endif #ifdef LIBCORO_FEATURE_TLS // For SSL/TLS tests create a localhost cert.pem and key.pem, tests expected these files diff --git a/test/net/test_tcp_server.cpp b/test/net/test_tcp_server.cpp index 5a9949c9..76f58db5 100644 --- a/test/net/test_tcp_server.cpp +++ b/test/net/test_tcp_server.cpp @@ -25,8 +25,6 @@ TEST_CASE("tcp_server ping server", "[tcp_server]") auto cstatus = co_await client.connect(); REQUIRE(cstatus == coro::net::connect_status::connected); - // Skip polling for write, should really only poll if the write is partial, shouldn't be - // required for this test. std::cerr << "client write()\n"; auto [sstatus, remaining] = co_await client.write(client_msg); REQUIRE(sstatus == coro::net::write_status::ok); @@ -45,23 +43,20 @@ TEST_CASE("tcp_server ping server", "[tcp_server]") }; auto make_server_task = [](std::shared_ptr scheduler, - const std::string& client_msg, - const std::string& server_msg) -> coro::task + const std::string& client_msg, + const std::string& server_msg) -> coro::task { co_await scheduler->schedule(); coro::net::tcp::server server{scheduler}; - // Poll for client connection. - std::cerr << "server poll(accept)\n"; - auto pstatus = co_await server.poll(); - REQUIRE(pstatus == coro::poll_status::event); - std::cerr << "server accept()\n"; - auto client = server.accept(); - REQUIRE(client.socket().is_valid()); + std::cerr << "server accept_client()\n"; + auto client = co_await server.accept_client(); + REQUIRE(client); + REQUIRE(client->socket().is_valid()); std::string buffer(256, '\0'); std::cerr << "server read()\n"; - auto [rstatus, rspan] = co_await client.read(buffer); + auto [rstatus, rspan] = co_await client->read(buffer); REQUIRE(rstatus == coro::net::read_status::ok); REQUIRE(rspan.size() == client_msg.size()); buffer.resize(rspan.size()); @@ -69,7 +64,7 @@ TEST_CASE("tcp_server ping server", "[tcp_server]") // Respond to client. std::cerr << "server send()\n"; - auto [sstatus, remaining] = co_await client.write(server_msg); + auto [sstatus, remaining] = co_await client->write(server_msg); REQUIRE(sstatus == coro::net::write_status::ok); REQUIRE(remaining.empty()); @@ -77,17 +72,20 @@ TEST_CASE("tcp_server ping server", "[tcp_server]") co_return; }; - coro::sync_wait(coro::when_all( - make_server_task(scheduler, client_msg, server_msg), make_client_task(scheduler, client_msg, server_msg))); + coro::sync_wait( + coro::when_all( + make_server_task(scheduler, client_msg, server_msg), make_client_task(scheduler, client_msg, server_msg))); } + #if defined(CORO_PLATFORM_UNIX) TEST_CASE("tcp_server concurrent polling on the same socket", "[tcp_server]") { // Issue 224: This test duplicates a client and issues two different poll operations per coroutine. using namespace std::chrono_literals; - auto scheduler = coro::io_scheduler::make_shared(coro::io_scheduler::options{ - .execution_strategy = coro::io_scheduler::execution_strategy_t::process_tasks_inline}); + auto scheduler = coro::io_scheduler::make_shared( + coro::io_scheduler::options{ + .execution_strategy = coro::io_scheduler::execution_strategy_t::process_tasks_inline}); auto make_server_task = [](std::shared_ptr scheduler) -> coro::task { @@ -164,5 +162,6 @@ TEST_CASE("tcp_server concurrent polling on the same socket", "[tcp_server]") REQUIRE(request == response); } + #endif // CORO_PLATFORM_UNIX #endif // LIBCORO_FEATURE_NETWORKING diff --git a/test/net/test_udp_peers.cpp b/test/net/test_udp_peers.cpp index a79ddb5b..83fa4d50 100644 --- a/test/net/test_udp_peers.cpp +++ b/test/net/test_udp_peers.cpp @@ -17,8 +17,8 @@ TEST_CASE("udp one way") coro::net::udp::peer peer{scheduler}; coro::net::udp::peer::info peer_info{}; - auto [sstatus, remaining] = peer.sendto(peer_info, msg); - REQUIRE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await peer.write_to(peer_info, msg); + REQUIRE(sstatus == coro::net::write_status::ok); REQUIRE(remaining.empty()); co_return; @@ -31,12 +31,9 @@ TEST_CASE("udp one way") coro::net::udp::peer self{scheduler, self_info}; - auto pstatus = co_await self.poll(coro::poll_op::read); - REQUIRE(pstatus == coro::poll_status::event); - std::string buffer(64, '\0'); - auto [rstatus, peer_info, rspan] = self.recvfrom(buffer); - REQUIRE(rstatus == coro::net::recv_status::ok); + auto [rstatus, peer_info, rspan] = co_await self.read_from(buffer); + REQUIRE(rstatus == coro::net::read_status::ok); REQUIRE(peer_info.address == coro::net::ip_address::from_string("127.0.0.1")); // The peer's port will be randomly picked by the kernel since it wasn't bound. REQUIRE(rspan.size() == msg.size()); @@ -74,19 +71,15 @@ TEST_CASE("udp echo peers") if (send_first) { // Send my message to my peer first. - auto [sstatus, remaining] = me.sendto(peer_info, my_msg); - REQUIRE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await me.write_to(peer_info, my_msg); + REQUIRE(sstatus == coro::net::write_status::ok); REQUIRE(remaining.empty()); } else { - // Poll for my peers message first. - auto pstatus = co_await me.poll(coro::poll_op::read); - REQUIRE(pstatus == coro::poll_status::event); - std::string buffer(64, '\0'); - auto [rstatus, recv_peer_info, rspan] = me.recvfrom(buffer); - REQUIRE(rstatus == coro::net::recv_status::ok); + auto [rstatus, recv_peer_info, rspan] = co_await me.read_from(buffer); + REQUIRE(rstatus == coro::net::read_status::ok); REQUIRE(recv_peer_info == peer_info); REQUIRE(rspan.size() == peer_msg.size()); buffer.resize(rspan.size()); @@ -95,13 +88,9 @@ TEST_CASE("udp echo peers") if (send_first) { - // I sent first so now I need to await my peer's message. - auto pstatus = co_await me.poll(coro::poll_op::read); - REQUIRE(pstatus == coro::poll_status::event); - std::string buffer(64, '\0'); - auto [rstatus, recv_peer_info, rspan] = me.recvfrom(buffer); - REQUIRE(rstatus == coro::net::recv_status::ok); + auto [rstatus, recv_peer_info, rspan] = co_await me.read_from(buffer); + REQUIRE(rstatus == coro::net::read_status::ok); REQUIRE(recv_peer_info == peer_info); REQUIRE(rspan.size() == peer_msg.size()); buffer.resize(rspan.size()); @@ -109,8 +98,8 @@ TEST_CASE("udp echo peers") } else { - auto [sstatus, remaining] = me.sendto(peer_info, my_msg); - REQUIRE(sstatus == coro::net::send_status::ok); + auto [sstatus, remaining] = co_await me.write_to(peer_info, my_msg); + REQUIRE(sstatus == coro::net::write_status::ok); REQUIRE(remaining.empty()); } diff --git a/test/test_io_scheduler.cpp b/test/test_io_scheduler.cpp index 88ceb3a3..105c1913 100644 --- a/test/test_io_scheduler.cpp +++ b/test/test_io_scheduler.cpp @@ -5,13 +5,16 @@ #include #include -#include #include +#include #include -#include -#include -#include + +#if defined(CORO_PLATFORM_UNIX) + #include + #include + #include +#endif // CORO_PLATFORM_UNIX TEST_CASE("io_scheduler", "[io_scheduler]") { @@ -113,6 +116,7 @@ TEST_CASE("io_scheduler task with multiple events", "[io_scheduler]") REQUIRE(s->empty()); } +#if defined(CORO_PLATFORM_UNIX) TEST_CASE("io_scheduler task with read poll", "[io_scheduler]") { auto trigger_fds = std::array{}; @@ -207,6 +211,7 @@ TEST_CASE("io_scheduler task with read poll timeout", "[io_scheduler]") close(trigger_fds[0]); close(trigger_fds[1]); } +#endif // CORO_PLATFORM_UNIX TEST_CASE("io_scheduler separate thread resume", "[io_scheduler]") { @@ -652,6 +657,7 @@ TEST_CASE("io_scheduler self generating coroutine (stack overflow check)", "[io_ REQUIRE(s->empty()); } +#if defined(CORO_PLATFORM_UNIX) TEST_CASE("io_scheduler manual process events thread pool", "[io_scheduler]") { auto trigger_fds = std::array{}; @@ -769,6 +775,7 @@ TEST_CASE("io_scheduler manual process events inline", "[io_scheduler]") close(trigger_fds[0]); close(trigger_fds[1]); } +#endif // CORO_PLATFORM_UNIX TEST_CASE("io_scheduler task throws", "[io_scheduler]") { From 8639ed53da0670dbaac9451e13c739f2c9d0447a Mon Sep 17 00:00:00 2001 From: PyXiion Date: Thu, 10 Jul 2025 00:04:29 +0300 Subject: [PATCH 18/24] Fixes Linux tests, compilation, and UDP functionality. --- include/coro/net/ip_address.hpp | 4 +++- include/coro/net/tcp/client.hpp | 9 ++++---- include/coro/net/tcp/server.hpp | 4 ++-- include/coro/net/udp/peer.hpp | 37 ++++++++++++++------------------ src/detail/io_notifier_epoll.cpp | 4 ++-- src/detail/timer_handle.cpp | 2 ++ src/net/ip_address.cpp | 19 ++++++++++++++++ src/net/socket.cpp | 9 +++++--- src/net/tcp/client.cpp | 3 +++ src/net/tcp/server.cpp | 20 +++++++++++++---- src/net/udp/peer.cpp | 9 ++++++-- 11 files changed, 81 insertions(+), 39 deletions(-) diff --git a/include/coro/net/ip_address.hpp b/include/coro/net/ip_address.hpp index 2dde563c..b2b2fb88 100644 --- a/include/coro/net/ip_address.hpp +++ b/include/coro/net/ip_address.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -66,7 +67,8 @@ class ip_address auto operator<=>(const ip_address& other) const = default; - auto to_os(std::uint16_t port, sockaddr_storage& storage, std::size_t& len) const -> void; + auto to_os(std::uint16_t port, sockaddr_storage& storage, std::size_t& len) const -> void; + static auto from_os(const sockaddr_storage& storage, std::size_t len) -> std::pair; static auto get_any_address(domain_t domain) -> ip_address; diff --git a/include/coro/net/tcp/client.hpp b/include/coro/net/tcp/client.hpp index f548efb3..5af858d1 100644 --- a/include/coro/net/tcp/client.hpp +++ b/include/coro/net/tcp/client.hpp @@ -152,6 +152,7 @@ class client std::optional m_connect_status{std::nullopt}; }; +template #if defined(CORO_PLATFORM_UNIX) auto client::recv(buffer_type&& buffer) -> std::pair> { @@ -201,7 +202,7 @@ auto client::send(const buffer_type& buffer) -> std::pair buffer, std::chrono::milliseconds timeout) +inline auto client::write(std::span buffer, std::chrono::milliseconds timeout) -> task>> { if (auto status = co_await poll(poll_op::write, timeout); status != poll_status::event) @@ -218,7 +219,7 @@ auto client::write(std::span buffer, std::chrono::milliseconds timeo throw std::runtime_error("Unknown poll_status value."); } } - switch (auto &&[status, span] = send(std::forward(buffer)); status) + switch (auto &&[status, span] = send(std::move(buffer)); status) { case send_status::ok: co_return {write_status::ok, span}; @@ -229,7 +230,7 @@ auto client::write(std::span buffer, std::chrono::milliseconds timeo } } -auto client::read(std::span buffer, std::chrono::milliseconds timeout) -> task>> +inline auto client::read(std::span buffer, std::chrono::milliseconds timeout) -> task>> { if (auto status = co_await poll(poll_op::read, timeout); status != poll_status::event) { @@ -245,7 +246,7 @@ auto client::read(std::span buffer, std::chrono::milliseconds timeout) -> throw std::runtime_error("Unknown poll_status value."); } } - switch (auto&& [status, span] = recv(std::forward(buffer)); status) + switch (auto&& [status, span] = recv(std::move(buffer)); status) { case recv_status::ok: co_return {read_status::ok, span}; diff --git a/include/coro/net/tcp/server.hpp b/include/coro/net/tcp/server.hpp index 79e87c14..6e42ec56 100644 --- a/include/coro/net/tcp/server.hpp +++ b/include/coro/net/tcp/server.hpp @@ -65,7 +65,7 @@ class server * @return The newly connected tcp client connection. * @note Unix only */ - auto accept() -> coro::net::tcp::client; + auto accept() const -> coro::net::tcp::client; #endif /** @@ -75,7 +75,7 @@ class server * @return A task resolving to an optional TCP client connection. The value will be set if a client was * successfully accepted, or std::nullopt if the operation timed out or was cancelled. */ - auto accept_client(std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task>; + auto accept_client() -> coro::task>; private: friend client; diff --git a/include/coro/net/udp/peer.hpp b/include/coro/net/udp/peer.hpp index 1e824cae..b6457b35 100644 --- a/include/coro/net/udp/peer.hpp +++ b/include/coro/net/udp/peer.hpp @@ -150,8 +150,8 @@ auto peer::recvfrom(buffer_type&& buffer) -> std::tuple{}}; } - sockaddr_in peer{}; - socklen_t peer_len{sizeof(peer)}; + sockaddr_storage peer{}; + socklen_t peer_len{sizeof(peer)}; auto bytes_read = ::recvfrom( m_socket.native_handle(), buffer.data(), buffer.size(), 0, reinterpret_cast(&peer), &peer_len); @@ -161,25 +161,20 @@ auto peer::recvfrom(buffer_type&& buffer) -> std::tuple(errno), peer::info{}, std::span{}}; } - std::span ip_addr_view{ - reinterpret_cast(&peer.sin_addr.s_addr), - sizeof(peer.sin_addr.s_addr), - }; + auto&& [address, port] = ip_address::from_os(peer, peer_len); return { recv_status::ok, peer::info{ - .address = net::ip_address{ip_addr_view, static_cast(peer.sin_family)}, - .port = ntohs(peer.sin_port)}, + .address = std::move(address), + .port = port + }, std::span{buffer.data(), static_cast(bytes_read)}}; } -auto peer::write_to( - const info& peer_info, - std::span buffer, - std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> coro::task>>; +inline auto peer::write_to(const info& peer_info, std::span buffer, std::chrono::milliseconds timeout) + -> coro::task>> { - if (auto status = co_await poll(poll_op::write, timeout)) + if (auto status = co_await poll(poll_op::write, timeout); status != poll_status::event) { switch (status) { @@ -194,7 +189,7 @@ auto peer::write_to( throw std::runtime_error("Unknown poll_status value."); } } - switch (auto&& [status, span] = sendto(peer_info, std::forward(buffer)); status) + switch (auto&& [status, span] = sendto(peer_info, buffer); status) { case send_status::ok: co_return {write_status::ok, span}; @@ -205,12 +200,12 @@ auto peer::write_to( } } -auto peer::read_from(std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) +inline auto peer::read_from(std::span buffer, std::chrono::milliseconds timeout) -> coro::task>> { if (!m_bound) { - return {recv_status::udp_not_bound, peer::info{}, std::span{}}; + co_return {read_status::udp_not_bound, peer::info{}, std::span{}}; } if (auto status = co_await poll(poll_op::read, timeout); status != poll_status::event) @@ -218,16 +213,16 @@ auto peer::read_from(std::span buffer, std::chrono::milliseconds timeout = switch (status) { case poll_status::closed: - co_return {read_status::closed, std::span{}}; + co_return {read_status::closed, peer::info{}, std::span{}}; case poll_status::error: - co_return {read_status::error, std::span{}}; + co_return {read_status::error, peer::info{}, std::span{}}; case poll_status::timeout: - co_return {read_status::timeout, std::span{}}; + co_return {read_status::timeout, peer::info{}, std::span{}}; default: throw std::runtime_error("Unknown poll_status value."); } } - switch (auto&& [status, info, span] = recvfrom(std::forward(buffer)); status) + switch (auto&& [status, info, span] = recvfrom(buffer); status) { case recv_status::ok: co_return {read_status::ok, std::move(info), span}; diff --git a/src/detail/io_notifier_epoll.cpp b/src/detail/io_notifier_epoll.cpp index 3f45eed6..4366a822 100644 --- a/src/detail/io_notifier_epoll.cpp +++ b/src/detail/io_notifier_epoll.cpp @@ -47,7 +47,7 @@ auto io_notifier_epoll::watch_timer(const detail::timer_handle& timer, std::chro itimerspec ts{}; ts.it_value.tv_sec = seconds.count(); ts.it_value.tv_nsec = nanoseconds.count(); - return ::timerfd_settime(timer.get_fd(), 0, &ts, nullptr) != -1; + return ::timerfd_settime(timer.get_native_handle(), 0, &ts, nullptr) != -1; } auto io_notifier_epoll::watch(fd_t fd, coro::poll_op op, void* data, bool keep) -> bool @@ -85,7 +85,7 @@ auto io_notifier_epoll::unwatch_timer(const detail::timer_handle& timer) -> bool itimerspec ts{}; ts.it_value.tv_sec = 0; ts.it_value.tv_nsec = 0; - return ::timerfd_settime(timer.get_fd(), 0, &ts, nullptr) != -1; + return ::timerfd_settime(timer.get_native_handle(), 0, &ts, nullptr) != -1; } auto io_notifier_epoll::next_events( diff --git a/src/detail/timer_handle.cpp b/src/detail/timer_handle.cpp index 651e4aff..1ea7c1b3 100644 --- a/src/detail/timer_handle.cpp +++ b/src/detail/timer_handle.cpp @@ -1,7 +1,9 @@ #include "coro/detail/timer_handle.hpp" #include "coro/io_notifier.hpp" +#if defined(CORO_PLATFORM_WINDOWS) #include +#endif namespace coro::detail { diff --git a/src/net/ip_address.cpp b/src/net/ip_address.cpp index 60dd7f9e..7e746905 100644 --- a/src/net/ip_address.cpp +++ b/src/net/ip_address.cpp @@ -1,5 +1,6 @@ #include "coro/net/ip_address.hpp" #include +#include #if defined(CORO_PLATFORM_UNIX) #include @@ -105,6 +106,24 @@ auto ip_address::to_os(const std::uint16_t port, sockaddr_storage& storage, std: throw std::runtime_error{"coro::net::ip_address unknown domain"}; } } +auto ip_address::from_os(const sockaddr_storage& storage, std::size_t len) -> std::pair +{ + if (storage.ss_family == AF_INET) + { + auto& addr = reinterpret_cast(storage); + const std::span ip_addr_view{ + reinterpret_cast(&addr.sin_addr.s_addr), sizeof(addr.sin_addr.s_addr)}; + + return {ip_address{ip_addr_view, domain_t::ipv4}, ntohs(addr.sin_port)}; + } + else + { + auto& addr = reinterpret_cast(storage); + const std::span ip_addr_view{reinterpret_cast(&addr.sin6_addr), sizeof(addr.sin6_addr)}; + + return {ip_address{ip_addr_view, domain_t::ipv6}, ntohs(addr.sin6_port)}; + } +} auto ip_address::get_any_address(domain_t domain) -> ip_address { switch (domain) diff --git a/src/net/socket.cpp b/src/net/socket.cpp index 1cf697cf..82dc8c6f 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -173,17 +173,20 @@ auto make_accept_socket(const socket::options& opts, const net::ip_address& addr // BSD and macOS use a different SO_REUSEPORT implementation than Linux that enables both duplicate address and port // bindings with a single flag. #if defined(CORO_PLATFORM_LINUX) + using socket_t = decltype(s.native_handle()); int sock_opt_name = SO_REUSEADDR | SO_REUSEPORT; int* sock_opt_ptr = &sock_opt; #elif defined(CORO_PLATFORM_BSD) + using socket_t = decltype(s.native_handle()); int sock_opt_name = SO_REUSEPORT; int* sock_opt_ptr = &sock_opt; #elif defined(CORO_PLATFORM_WINDOWS) + using socket_t = SOCKET; int sock_opt_name = SO_REUSEADDR; const char* sock_opt_ptr = reinterpret_cast(&sock_opt); #endif - if (setsockopt((SOCKET)s.native_handle(), SOL_SOCKET, sock_opt_name, sock_opt_ptr, sizeof(sock_opt)) < 0) + if (setsockopt(reinterpret_cast(s.native_handle()), SOL_SOCKET, sock_opt_name, sock_opt_ptr, sizeof(sock_opt)) < 0) { throw std::runtime_error{"Failed to setsockopt."}; } @@ -192,14 +195,14 @@ auto make_accept_socket(const socket::options& opts, const net::ip_address& addr std::size_t server_len{}; address.to_os(port, server, server_len); - if (bind(reinterpret_cast(s.native_handle()), reinterpret_cast(&server), server_len) < 0) + if (bind(reinterpret_cast(s.native_handle()), reinterpret_cast(&server), server_len) < 0) { throw std::runtime_error{"Failed to bind."}; } if (opts.type == socket::type_t::tcp) { - if (listen(reinterpret_cast(s.native_handle()), backlog) < 0) + if (listen(reinterpret_cast(s.native_handle()), backlog) < 0) { throw std::runtime_error{"Failed to listen."}; } diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index 513a0533..4f92364b 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -1,4 +1,6 @@ #include "coro/net/tcp/client.hpp" + +#if defined(CORO_PLATFORM_WINDOWS) // The order of includes matters // clang-format off #include @@ -6,6 +8,7 @@ #include #include // clang-format on +#endif namespace coro::net::tcp { diff --git a/src/net/tcp/server.cpp b/src/net/tcp/server.cpp index 74d792be..a655d243 100644 --- a/src/net/tcp/server.cpp +++ b/src/net/tcp/server.cpp @@ -2,13 +2,15 @@ #include "coro/io_scheduler.hpp" -// The order of includes matters -// clang-format off +#if defined(CORO_PLATFORM_WINDOWS) + // The order of includes matters + // clang-format off #define WIN32_LEAN_AND_MEAN #include #include #include #include "coro/detail/iocp_overlapped.hpp" +#endif // clang-format on @@ -53,13 +55,13 @@ auto server::operator=(server&& other) -> server& } #if defined(CORO_PLATFORM_UNIX) -auto server::accept() -> coro::net::tcp::client +auto server::accept() const -> coro::net::tcp::client { sockaddr_in client{}; constexpr const int len = sizeof(struct sockaddr_in); net::socket s{reinterpret_cast(::accept( - reinterpret_cast(m_accept_socket.native_handle()), + m_accept_socket.native_handle(), reinterpret_cast(&client), const_cast(reinterpret_cast(&len))))}; @@ -79,6 +81,16 @@ auto server::accept() -> coro::net::tcp::client auto server::accept_client() -> coro::task> { + switch (co_await poll()) + { + case poll_status::event: + break; // ignoring + case poll_status::closed: + case poll_status::error: + case poll_status::timeout: + co_return std::nullopt; + } + co_return accept(); } #elif defined(CORO_PLATFORM_WINDOWS) auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::task> diff --git a/src/net/udp/peer.cpp b/src/net/udp/peer.cpp index 7dbe56bd..ad9fe06c 100644 --- a/src/net/udp/peer.cpp +++ b/src/net/udp/peer.cpp @@ -1,12 +1,14 @@ #include "coro/net/udp/peer.hpp" -// The order of includes matters -// clang-format off +#if defined(CORO_PLATFORM_WINDOWS) + // The order of includes matters + // clang-format off #include #include #include #include "coro/detail/iocp_overlapped.hpp" // clang-format on +#endif namespace coro::net::udp { @@ -34,6 +36,8 @@ peer::peer(std::shared_ptr scheduler, const info& bind_info) m_io_scheduler->bind_socket(m_socket); #endif } + +#if defined(CORO_PLATFORM_WINDOWS) auto peer::write_to(const info& peer_info, std::span buffer, std::chrono::milliseconds timeout) -> coro::task>> { @@ -179,5 +183,6 @@ auto peer::read_from(std::span buffer, std::chrono::milliseconds timeout) co_return {read_status::error, peer::info{}, std::span{}}; } +#endif } // namespace coro::net::udp From a009c983d5d44c329272b1f744f32f7b8f4c83dd Mon Sep 17 00:00:00 2001 From: PyXiion Date: Thu, 10 Jul 2025 00:56:51 +0300 Subject: [PATCH 19/24] Add notes and clarify code behavior in IOCP implementation details. --- include/coro/detail/io_notifier_iocp.hpp | 1 + include/coro/net/udp/peer.hpp | 44 +++++++++++++----------- include/coro/signal.hpp | 9 +++-- src/detail/io_notifier_iocp.cpp | 42 ++++++++++++++++++++++ 4 files changed, 74 insertions(+), 22 deletions(-) diff --git a/include/coro/detail/io_notifier_iocp.hpp b/include/coro/detail/io_notifier_iocp.hpp index 1d36698f..7ce9dc44 100644 --- a/include/coro/detail/io_notifier_iocp.hpp +++ b/include/coro/detail/io_notifier_iocp.hpp @@ -9,6 +9,7 @@ namespace coro::detail { class timer_handle; + class io_notifier_iocp { public: diff --git a/include/coro/net/udp/peer.hpp b/include/coro/net/udp/peer.hpp index b6457b35..66a30d5f 100644 --- a/include/coro/net/udp/peer.hpp +++ b/include/coro/net/udp/peer.hpp @@ -35,13 +35,13 @@ class peer }; /** - * Creates a udp peer that can send packets but not receive them. This udp peer will not explicitly + * Creates an udp peer that can send packets but not receive them. This udp peer will not explicitly * bind to a local ip+port. */ explicit peer(std::shared_ptr scheduler, net::domain_t domain = net::domain_t::ipv4); /** - * Creates a udp peer that can send and receive packets. This peer will bind to the given ip_port. + * Creates an udp peer that can send and receive packets. This peer will bind to the given ip_port. */ explicit peer(std::shared_ptr scheduler, const info& bind_info); @@ -57,6 +57,7 @@ class peer * udp socket (did not bind) then polling for read will not work. * @param timeout The timeout for the poll operation to be ready. * @return The result status of the poll operation. + * @note Unix only */ auto poll(poll_op op, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task @@ -69,40 +70,46 @@ class peer * @param buffer The data to send. * @return The status of send call and a span view of any data that wasn't sent. This data if * un-sent will correspond to bytes at the end of the given buffer. + * @note Unix only */ template auto sendto(const info& peer_info, const buffer_type& buffer) -> std::pair>; /** * @param buffer The buffer to receive data into. - * @return The receive status, if ok then also the peer who sent the data and the data. + * @return The reception status, if OK then also the peer who sent the data and the data. * The span view of the data will be set to the size of the received data, this will - * always start at the beggining of the buffer but depending on how large the data was + * always start at the beginning of the buffer but depending on how large the data was * it might not fill the entire buffer. + * @note Unix only */ template auto recvfrom(buffer_type&& buffer) -> std::tuple>; +#endif + /** + * @param peer_info The peer to send the data to. + * @param buffer The data to send. + * @param timeout The timeout for the operation to be ready. + * @return The status of write call and a span view of any data that wasn't sent. This data if + * un-sent will correspond to bytes at the end of the given buffer. + */ auto write_to( const info& peer_info, std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task>>; + /** + * @param buffer The buffer to receive data into. + * @param timeout The timeout for the operation to be ready. + * @return The reception status, if OK then also the peer who sent the data and the data. + * The span view of the data will be set to the size of the received data, this will + * always start at the beginning of the buffer but depending on how large the data was + * it might not fill the entire buffer. + */ auto read_from(std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task>>; -#elif defined(CORO_PLATFORM_WINDOWS) - - auto write_to( - const info& peer_info, - std::span buffer, - std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> coro::task>>; - - auto read_from(std::span buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) - -> coro::task>>; - -#endif private: /// The scheduler that will drive this udp client. @@ -165,10 +172,7 @@ auto peer::recvfrom(buffer_type&& buffer) -> std::tuple{buffer.data(), static_cast(bytes_read)}}; } inline auto peer::write_to(const info& peer_info, std::span buffer, std::chrono::milliseconds timeout) diff --git a/include/coro/signal.hpp b/include/coro/signal.hpp index 43ca7e4f..a2613c4d 100644 --- a/include/coro/signal.hpp +++ b/include/coro/signal.hpp @@ -1,3 +1,8 @@ +/** +* When the signal is active, it will post its data to the io_notifier +* every next_events call. + */ + #pragma once #include "coro/platform.hpp" @@ -10,8 +15,8 @@ namespace coro { #if defined(CORO_PLATFORM_UNIX) - using signal = detail::signal_unix; +using signal = detail::signal_unix; #elif defined(CORO_PLATFORM_WINDOWS) - using signal = detail::signal_win32; +using signal = detail::signal_win32; #endif } // namespace coro \ No newline at end of file diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp index 947988ed..12b53bf0 100644 --- a/src/detail/io_notifier_iocp.cpp +++ b/src/detail/io_notifier_iocp.cpp @@ -69,6 +69,48 @@ auto io_notifier_iocp::watch(const coro::signal& signal, void* data) -> bool return true; } +/** + * I think this cycle needs a little explanation. + * + * == Completion keys == + * + * 1. **Signals** + * IOCP is not like epoll or kqueue, it works only with file-related events. + * To emulate signals io_scheduler uses (previously a pipe, now abstracted into signals) + * I use an array that tracks all active signals and dispatches them on every call. + * + * Because of this, we need a pointer to the IOCP handle inside `@ref coro::signal`. + * + * 2. **Sockets** + * It's nothing special. We just get the pointer to poll_info through `@ref coro::detail::overlapped_poll_info`. + * The overlapped structure is stored inside the coroutine, so as long as coroutine lives everything will be fine. + * But if the coroutine dies, it's UB. I see no point in using heap, since if we have no coroutine, what should + * we dispatch? + * + * **Important** + * All sockets **must** have the following flags set using `SetFileCompletionNotificationModes`: + * + * - `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS`: + * Prevents IOCP from enqueuing completions if the operation completes synchronously. + * If disabled, IOCP might try to access an `OVERLAPPED` structure from a coroutine that has already died. + * This can cause undefined behavior if the coroutine is dead and its memory is invalid. + * If it's still alive - you got lucky. + * + * - `FILE_SKIP_SET_EVENT_ON_HANDLE`: + * Prevents the system from setting a WinAPI event on the socket handle. + * We don’t use system events, so this is safe and gives a small performance boost. + * + * 3. Timers + * IOCP doesn’t support timers directly - Windows has no `timerfd` like Unix. + * We use waitable timers (see `timer_handle.cpp`) to emulate this. + * When the timer fires, it triggers `@ref onTimerFired`, which posts an event to the IOCP queue. + * Since it's our own event we don't have to pass a valid OVERLAPPED structure, + * we just pass a pointer to the timer data and then emplace it into `ready_events`. + * + * **The cycle itself** + * Rewrite to GetQueuedCompletionStatusEx + * + */ auto io_notifier_iocp::next_events( std::vector>& ready_events, const std::chrono::milliseconds timeout, From bbb274f459b0b17cb6d640158847f69adefde9a4 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Thu, 10 Jul 2025 00:56:58 +0300 Subject: [PATCH 20/24] Remove unused file --- crosscompile/Dockerfile | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 crosscompile/Dockerfile diff --git a/crosscompile/Dockerfile b/crosscompile/Dockerfile deleted file mode 100644 index b7bf0a64..00000000 --- a/crosscompile/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM ubuntu:22.04 - -RUN apt update && apt install -y \ - mingw-w64 \ - cmake \ - ninja-build \ - zip \ - git \ - g++ \ - build-essential \ - && apt clean - -ARG UID=1000 -ARG GID=1000 - -RUN groupadd -g ${GID} builder && \ - useradd -m -u ${UID} -g ${GID} -s /bin/bash builder - -RUN apt update && apt install -y \ - mingw-w64 \ - cmake \ - ninja-build \ - build-essential - -USER builder -WORKDIR /home/builder/project \ No newline at end of file From 6a2f19d9da3b6833794b1e34b02cfdbf4bf7ec8d Mon Sep 17 00:00:00 2001 From: PyXiion Date: Thu, 10 Jul 2025 01:16:09 +0300 Subject: [PATCH 21/24] io_notifier_iocp: using GetQueuedCompletionStatusEx instead of GetQueuedCompletionStatus for batched events --- include/coro/detail/io_notifier_iocp.hpp | 6 +- include/coro/net/tcp/client.hpp | 2 +- include/coro/net/tcp/server.hpp | 2 +- src/detail/io_notifier_iocp.cpp | 73 +++++++----------------- src/net/tcp/server.cpp | 6 +- 5 files changed, 28 insertions(+), 61 deletions(-) diff --git a/include/coro/detail/io_notifier_iocp.hpp b/include/coro/detail/io_notifier_iocp.hpp index 7ce9dc44..de49102e 100644 --- a/include/coro/detail/io_notifier_iocp.hpp +++ b/include/coro/detail/io_notifier_iocp.hpp @@ -9,7 +9,6 @@ namespace coro::detail { class timer_handle; - class io_notifier_iocp { public: @@ -39,8 +38,7 @@ class io_notifier_iocp auto next_events( std::vector>& ready_events, - std::chrono::milliseconds timeout, - size_t max_events = 16) -> void; + std::chrono::milliseconds timeout) -> void; // static auto event_to_poll_status(const event_t& event) -> poll_status; @@ -54,5 +52,7 @@ class io_notifier_iocp std::mutex m_active_signals_mutex; std::vector m_active_signals; + + static constexpr std::size_t max_events = 16; }; } // namespace coro::detail \ No newline at end of file diff --git a/include/coro/net/tcp/client.hpp b/include/coro/net/tcp/client.hpp index 5af858d1..21453654 100644 --- a/include/coro/net/tcp/client.hpp +++ b/include/coro/net/tcp/client.hpp @@ -152,8 +152,8 @@ class client std::optional m_connect_status{std::nullopt}; }; -template #if defined(CORO_PLATFORM_UNIX) +template auto client::recv(buffer_type&& buffer) -> std::pair> { // If the user requested zero bytes, just return. diff --git a/include/coro/net/tcp/server.hpp b/include/coro/net/tcp/server.hpp index 6e42ec56..87af6176 100644 --- a/include/coro/net/tcp/server.hpp +++ b/include/coro/net/tcp/server.hpp @@ -75,7 +75,7 @@ class server * @return A task resolving to an optional TCP client connection. The value will be set if a client was * successfully accepted, or std::nullopt if the operation timed out or was cancelled. */ - auto accept_client() -> coro::task>; + auto accept_client(std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task>; private: friend client; diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp index 12b53bf0..cc876dfd 100644 --- a/src/detail/io_notifier_iocp.cpp +++ b/src/detail/io_notifier_iocp.cpp @@ -1,7 +1,9 @@ #include "coro/detail/io_notifier_iocp.hpp" #include "coro/detail/signal_win32.hpp" #include "coro/detail/timer_handle.hpp" + #include +#include #include namespace coro::detail @@ -107,14 +109,10 @@ auto io_notifier_iocp::watch(const coro::signal& signal, void* data) -> bool * Since it's our own event we don't have to pass a valid OVERLAPPED structure, * we just pass a pointer to the timer data and then emplace it into `ready_events`. * - * **The cycle itself** - * Rewrite to GetQueuedCompletionStatusEx - * */ auto io_notifier_iocp::next_events( std::vector>& ready_events, - const std::chrono::milliseconds timeout, - const size_t max_events) -> void + const std::chrono::milliseconds timeout) -> void { using namespace std::chrono; @@ -148,62 +146,31 @@ auto io_notifier_iocp::next_events( process_active_signals(ready_events); - if (timeout.count() >= 0) - { - milliseconds remaining = timeout; - while (remaining.count() > 0 && ready_events.size() < max_events) - { - auto t0 = steady_clock::now(); - DWORD bytes = 0; - completion_key key{}; - LPOVERLAPPED ov = nullptr; - - BOOL ok = GetQueuedCompletionStatus( - m_iocp, &bytes, reinterpret_cast(&key), &ov, static_cast(remaining.count())); - auto t1 = steady_clock::now(); - - if (!ok && ov == nullptr) - { - break; - } - - handle(bytes, key, ov); + std::array entries{}; + ULONG number_of_events{}; + const DWORD dword_timeout = (timeout <= 0ms) ? INFINITE : static_cast(timeout.count()); - auto took = duration_cast(t1 - t0); - if (took < remaining) - remaining -= took; - else - break; - } - } - else + if (BOOL ok = GetQueuedCompletionStatusEx( + m_iocp, entries.data(), entries.size(), &number_of_events, dword_timeout, FALSE); + !ok) { - DWORD bytes = 0; - completion_key key{}; - LPOVERLAPPED ov = nullptr; - BOOL ok = GetQueuedCompletionStatus(m_iocp, &bytes, reinterpret_cast(&key), &ov, INFINITE); - - if (!ok && ov == nullptr) + const DWORD err = GetLastError(); + if (err == WAIT_TIMEOUT) { + // No events available return; } - handle(bytes, key, ov); + + throw std::system_error(static_cast(err), std::system_category(), "GetQueuedCompletionStatusEx failed."); } - while (ready_events.size() < max_events) + for (ULONG i = 0; i < number_of_events; ++i) { - DWORD bytes = 0; - completion_key key{}; - LPOVERLAPPED ov = nullptr; - BOOL ok = GetQueuedCompletionStatus( - m_iocp, - &bytes, - reinterpret_cast(&key), - &ov, - 0 // non-blocking - ); - if (!ok && ov == nullptr) - break; + const auto& e = entries[i]; + const auto key = static_cast(e.lpCompletionKey); + const auto ov = e.lpOverlapped; + const auto bytes = e.dwNumberOfBytesTransferred; + handle(bytes, key, ov); } } diff --git a/src/net/tcp/server.cpp b/src/net/tcp/server.cpp index a655d243..e10e2ac2 100644 --- a/src/net/tcp/server.cpp +++ b/src/net/tcp/server.cpp @@ -48,7 +48,7 @@ auto server::operator=(server&& other) -> server& if (std::addressof(other) != this) { m_io_scheduler = std::move(other.m_io_scheduler); - m_options = std::move(other.m_options); + m_options = other.m_options; m_accept_socket = std::move(other.m_accept_socket); } return *this; @@ -79,9 +79,9 @@ auto server::accept() const -> coro::net::tcp::client }}; }; -auto server::accept_client() -> coro::task> +auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::task> { - switch (co_await poll()) + switch (co_await poll(timeout)) { case poll_status::event: break; // ignoring From 7dc95623187f024afba330331cbeee5c97090b18 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sun, 13 Jul 2025 22:30:58 +0300 Subject: [PATCH 22/24] timer_handle: remove mutables, use RegisterWaitForSingleObject instead of thread-alert callbacks --- include/coro/detail/io_notifier_iocp.hpp | 4 +- include/coro/detail/timer_handle.hpp | 39 +++++++++----- src/detail/io_notifier_iocp.cpp | 66 +++++++++++++++++------- src/detail/timer_handle.cpp | 12 +++-- 4 files changed, 82 insertions(+), 39 deletions(-) diff --git a/include/coro/detail/io_notifier_iocp.hpp b/include/coro/detail/io_notifier_iocp.hpp index de49102e..6f279207 100644 --- a/include/coro/detail/io_notifier_iocp.hpp +++ b/include/coro/detail/io_notifier_iocp.hpp @@ -30,11 +30,11 @@ class io_notifier_iocp ~io_notifier_iocp(); - auto watch_timer(const detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool; + auto watch_timer(detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool; auto watch(const coro::signal& signal, void* data) -> bool; - auto unwatch_timer(const detail::timer_handle& timer) -> bool; + auto unwatch_timer(detail::timer_handle& timer) -> bool; auto next_events( std::vector>& ready_events, diff --git a/include/coro/detail/timer_handle.hpp b/include/coro/detail/timer_handle.hpp index 5fb50f2f..f0423e91 100644 --- a/include/coro/detail/timer_handle.hpp +++ b/include/coro/detail/timer_handle.hpp @@ -9,32 +9,47 @@ namespace coro namespace detail { +#if defined(CORO_PLATFORM_UNIX) class timer_handle { -#if defined(CORO_PLATFORM_UNIX) using native_handle_t = coro::fd_t; -#elif defined(CORO_PLATFORM_WINDOWS) - friend class io_notifier_iocp; - using native_handle_t = void *; - mutable void* m_iocp = nullptr; -#endif +public: + timer_handle(const void* timer_handle_ptr, io_notifier& notifier); + ~timer_handle(); + + native_handle_t get_native_handle() const { return m_native_handle; } + const void* get_inner() const { return m_timer_handle_ptr; } + +private: native_handle_t m_native_handle; - const void* m_timer_handle_ptr = nullptr; + const void* m_timer_handle_ptr = nullptr; +}; + +#elif defined(CORO_PLATFORM_WINDOWS) +class timer_handle +{ + friend class io_notifier_iocp; + using native_handle_t = void*; public: timer_handle(const void* timer_handle_ptr, io_notifier& notifier); ~timer_handle(); - native_handle_t get_native_handle() const { return m_native_handle; } + [[nodiscard]] native_handle_t get_native_handle() const { return m_native_handle; } - const void* get_inner() const { return m_timer_handle_ptr; } + [[nodiscard]] const void* get_inner() const { return m_timer_handle_ptr; } + [[nodiscard]] void* get_iocp() const { return m_iocp; } -#if defined(CORO_PLATFORM_WINDOWS) - void* get_iocp() const { return m_iocp; } -#endif +private: + native_handle_t m_native_handle; + + const void* m_timer_handle_ptr = nullptr; + void* m_iocp = nullptr; + void* m_wait_handle = nullptr; }; +#endif } // namespace detail diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp index cc876dfd..fcd3e380 100644 --- a/src/detail/io_notifier_iocp.cpp +++ b/src/detail/io_notifier_iocp.cpp @@ -20,19 +20,7 @@ io_notifier_iocp::~io_notifier_iocp() CloseHandle(m_iocp); } -static VOID CALLBACK onTimerFired(LPVOID timerPtr, DWORD, DWORD) -{ - auto* handle = static_cast(timerPtr); - - // Completion key 3 means timer - PostQueuedCompletionStatus( - handle->get_iocp(), - 0, - static_cast(io_notifier::completion_key::timer), - static_cast(const_cast(handle->get_inner()))); -} - -auto io_notifier_iocp::watch_timer(const detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool +auto io_notifier_iocp::watch_timer(detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool { if (timer.m_iocp == nullptr) { @@ -44,8 +32,7 @@ auto io_notifier_iocp::watch_timer(const detail::timer_handle& timer, std::chron } LARGE_INTEGER dueTime{}; - // time in 100ns intervals, negative for relative - dueTime.QuadPart = -duration.count() / 100; + dueTime.QuadPart = -duration.count() / 100; // time in 100ns intervals, negative for relative // `timer_handle` must remain alive until the timer fires. // This is guaranteed by `io_scheduler`, which owns the timer lifetime. @@ -56,12 +43,42 @@ auto io_notifier_iocp::watch_timer(const detail::timer_handle& timer, std::chron // // Therefore, we directly pass a pointer to `timer_handle` as the APC context. // This avoids allocations and should be safe (I hope) under our scheduler's lifetime guarantees. - return SetWaitableTimer(timer.get_native_handle(), &dueTime, 0, &onTimerFired, (void*)std::addressof(timer), FALSE); + + if (timer.m_wait_handle != nullptr) + { + unwatch_timer(timer); + } + + BOOL ok = SetWaitableTimer(timer.get_native_handle(), &dueTime, 0, nullptr, nullptr, false); + + if (!ok) + return false; + + ok = RegisterWaitForSingleObject( + &timer.m_wait_handle, + timer.get_native_handle(), + [](PVOID timer_ptr, BOOLEAN) + { + const auto timer = static_cast(timer_ptr); + PostQueuedCompletionStatus( + timer->get_iocp(), 0, static_cast(completion_key::timer), reinterpret_cast(timer)); + }, + &timer, + INFINITE, + WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION); + return ok; } -auto io_notifier_iocp::unwatch_timer(const detail::timer_handle& timer) -> bool +auto io_notifier_iocp::unwatch_timer(detail::timer_handle& timer) -> bool { - return CancelWaitableTimer(timer.get_native_handle()); + if (timer.m_wait_handle == nullptr) + { + return false; + } + CancelWaitableTimer(timer.get_native_handle()); + UnregisterWaitEx(timer.m_wait_handle, INVALID_HANDLE_VALUE); + timer.m_wait_handle = nullptr; + return true; } auto io_notifier_iocp::watch(const coro::signal& signal, void* data) -> bool @@ -137,7 +154,16 @@ auto io_notifier_iocp::next_events( break; case completion_key::timer: if (ov) - ready_events.emplace_back(reinterpret_cast(ov), coro::poll_status::event); + { + auto timer = reinterpret_cast(ov); + ready_events.emplace_back( + static_cast(const_cast(timer->get_inner())), + coro::poll_status::event); + + UnregisterWaitEx(timer->m_wait_handle, INVALID_HANDLE_VALUE); + timer->m_wait_handle = nullptr; + std::atomic_thread_fence(std::memory_order::release); + } break; default: throw std::runtime_error("Unknown completion key"); @@ -150,7 +176,7 @@ auto io_notifier_iocp::next_events( ULONG number_of_events{}; const DWORD dword_timeout = (timeout <= 0ms) ? INFINITE : static_cast(timeout.count()); - if (BOOL ok = GetQueuedCompletionStatusEx( + if (const BOOL ok = GetQueuedCompletionStatusEx( m_iocp, entries.data(), entries.size(), &number_of_events, dword_timeout, FALSE); !ok) { diff --git a/src/detail/timer_handle.cpp b/src/detail/timer_handle.cpp index 1ea7c1b3..43af303e 100644 --- a/src/detail/timer_handle.cpp +++ b/src/detail/timer_handle.cpp @@ -2,7 +2,7 @@ #include "coro/io_notifier.hpp" #if defined(CORO_PLATFORM_WINDOWS) -#include + #include #endif namespace coro::detail @@ -37,13 +37,15 @@ timer_handle::~timer_handle() #elif defined(CORO_PLATFORM_WINDOWS) timer_handle::timer_handle(const void* timer_handle_ptr, io_notifier& notifier) - : m_timer_handle_ptr(timer_handle_ptr), - m_native_handle(CreateWaitableTimer(NULL, FALSE, NULL)) + : m_native_handle(CreateWaitableTimerW(nullptr, FALSE, nullptr)), + m_timer_handle_ptr(timer_handle_ptr) { - if (m_native_handle == NULL) + if (m_native_handle == nullptr) { - throw std::runtime_error("Failed to CreateWaitableTimer"); + throw std::system_error( + static_cast(GetLastError()), std::system_category(), "Failed to CreateWaitableTimer"); } + (void)notifier; } timer_handle::~timer_handle() { From 9c7a26f07c85c21332475e5c450d064640762dca Mon Sep 17 00:00:00 2001 From: PyXiion Date: Sun, 13 Jul 2025 23:13:48 +0300 Subject: [PATCH 23/24] handle close on zero bytes in the client, generic function for WinSock read/write --- include/coro/detail/iocp_overlapped.hpp | 95 ++++++++++++++++++++++--- src/detail/io_notifier_iocp.cpp | 5 +- src/io_scheduler.cpp | 2 +- src/net/tcp/client.cpp | 88 +++-------------------- src/net/tcp/server.cpp | 3 +- 5 files changed, 99 insertions(+), 94 deletions(-) diff --git a/include/coro/detail/iocp_overlapped.hpp b/include/coro/detail/iocp_overlapped.hpp index 09646078..f9ba1236 100644 --- a/include/coro/detail/iocp_overlapped.hpp +++ b/include/coro/detail/iocp_overlapped.hpp @@ -2,23 +2,96 @@ // Do not include this file from headers. Include only in implementation files (.cpp) or modules. #pragma once -#include +#include "coro/io_scheduler.hpp" #include +// clang-format off +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#include +#include "coro/detail/iocp_overlapped.hpp" +// clang-format on + namespace coro::detail { struct overlapped_io_operation { - OVERLAPPED ov{}; // Base Windows OVERLAPPED structure for async I/O - poll_info pi; - std::size_t bytes_transferred{}; // Number of bytes read or written once the operation completes - - /** - * Marks this operation as an AcceptEx call: - * allows distinguishing accept events from normal reads/writes - * and ensures that a zero-byte completion isn’t treated as poll_status::closed - */ - bool is_accept{}; + OVERLAPPED ov{}; // Base Windows OVERLAPPED structure for async I/O + poll_info pi; + DWORD bytes_transferred{}; // Number of bytes read or written once the operation completes + + SOCKET socket{}; }; +template + requires std::is_invocable_r_v +auto perform_write_read_operation( + const std::shared_ptr& scheduler, + SOCKET socket, + operation_fn&& operation, + buffer_type buffer, + std::chrono::milliseconds timeout) -> task> +{ + overlapped_io_operation ov{}; + WSABUF buf{}; + + ov.socket = socket; + + buf.buf = const_cast(buffer.data()); + buf.len = buffer.size(); + + auto get_result_buffer = [&]() + { + if constexpr (is_read) + return ov.bytes_transferred == 0 ? buffer_type{} : buffer_type{buffer.data(), ov.bytes_transferred}; + else + return ov.bytes_transferred == 0 + ? buffer_type{} + : buffer_type{buffer.data() + ov.bytes_transferred, buffer.size() - ov.bytes_transferred}; + }; + + auto r = operation(socket, std::ref(ov), std::ref(buf)); + + // Operation has been completed synchronously, no need to wait for event. + if (r == 0) + { + co_return {ov.bytes_transferred == 0 ? status_enum::closed : status_enum::ok, get_result_buffer()}; + } + if (WSAGetLastError() != WSA_IO_PENDING) + { + co_return {status_enum::error, buffer}; + } + + // We need loop in case the operation completes right away with the timeout. + // In this case we just co_await our poll_info once more to correct status. + while (true) + { + switch (co_await scheduler->poll(ov.pi, timeout)) + { + case poll_status::event: + co_return {status_enum::ok, get_result_buffer()}; + case poll_status::timeout: + { + if (const BOOL success = CancelIoEx(reinterpret_cast(socket), &ov.ov); !success) + { + if (const auto err = GetLastError(); err == ERROR_NOT_FOUND) + { + // Operation has been completed, we need to co_await once more + timeout = {}; // No need in timeout + continue; + } + } + co_return {status_enum::timeout, get_result_buffer()}; + } + case poll_status::closed: + co_return {status_enum::closed, buffer}; + case poll_status::error: + default: + co_return {status_enum::error, buffer}; + } + } +} + } // namespace coro::detail \ No newline at end of file diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp index fcd3e380..5386371d 100644 --- a/src/detail/io_notifier_iocp.cpp +++ b/src/detail/io_notifier_iocp.cpp @@ -2,7 +2,6 @@ #include "coro/detail/signal_win32.hpp" #include "coro/detail/timer_handle.hpp" -#include #include #include @@ -147,9 +146,7 @@ auto io_notifier_iocp::next_events( { auto* info = reinterpret_cast(ov); info->bytes_transferred = bytes; - coro::poll_status st = - (bytes == 0 && !info->is_accept) ? coro::poll_status::closed : coro::poll_status::event; - ready_events.emplace_back(&info->pi, st); + ready_events.emplace_back(&info->pi, coro::poll_status::event); } break; case completion_key::timer: diff --git a/src/io_scheduler.cpp b/src/io_scheduler.cpp index 136f9f17..c2ebd136 100644 --- a/src/io_scheduler.cpp +++ b/src/io_scheduler.cpp @@ -200,7 +200,7 @@ auto io_scheduler::yield_for_internal(std::chrono::nanoseconds amount) -> coro:: // for the scheduled task there. m_size.fetch_add(1, std::memory_order::release); - // Yielding does not requiring setting the timer position on the poll info since + // Yielding does not require setting the timer position on the poll info since // it doesn't have a corresponding 'event' that can trigger, it always waits for // the timeout to occur before resuming. diff --git a/src/net/tcp/client.cpp b/src/net/tcp/client.cpp index 4f92364b..d47b06a6 100644 --- a/src/net/tcp/client.cpp +++ b/src/net/tcp/client.cpp @@ -177,7 +177,7 @@ auto client::connect(std::chrono::milliseconds timeout) -> coro::task(m_socket.native_handle()); // Bind socket first to local address sockaddr_storage local_addr_storage{}; @@ -251,90 +251,24 @@ auto client::poll(coro::poll_op op, std::chrono::milliseconds timeout) -> coro:: auto client::write(std::span buffer, std::chrono::milliseconds timeout) -> task>> { - detail::overlapped_io_operation ov{}; - WSABUF buf; - buf.buf = const_cast(buffer.data()); - buf.len = buffer.size(); - DWORD flags = 0, bytes_sent = 0; - - int r = WSASend(reinterpret_cast(m_socket.native_handle()), &buf, 1, &bytes_sent, flags, &ov.ov, nullptr); - if (r == 0) // Data already sent - { - if (bytes_sent == 0) - co_return {write_status::closed, buffer}; - co_return {write_status::ok, std::span{buffer.data() + bytes_sent, buffer.size() - bytes_sent}}; - } - else if (WSAGetLastError() == WSA_IO_PENDING) - { - auto status = co_await m_io_scheduler->poll(ov.pi, timeout); - if (status == poll_status::event) - { - co_return { - write_status::ok, - std::span{buffer.data() + ov.bytes_transferred, buffer.size() - ov.bytes_transferred}}; - } - else if (status == poll_status::timeout) - { - BOOL success = CancelIoEx(static_cast(m_socket.native_handle()), &ov.ov); - if (!success) - { - int err = GetLastError(); - if (err == ERROR_NOT_FOUND) - { - // Operation has been completed - co_return { - write_status::ok, - std::span{ - buffer.data() + ov.bytes_transferred, buffer.size() - ov.bytes_transferred}}; - } - } - co_return {write_status::timeout, buffer}; - } - } + static constexpr auto send_fn = [](SOCKET s, detail::overlapped_io_operation& ov, WSABUF& buf) + { return WSASend(s, &buf, 1, &ov.bytes_transferred, 0, &ov.ov, nullptr); }; - co_return {write_status::error, buffer}; + co_return co_await detail::perform_write_read_operation, false>( + m_io_scheduler, reinterpret_cast(m_socket.native_handle()), send_fn, buffer, timeout); } auto client::read(std::span buffer, std::chrono::milliseconds timeout) -> task>> { - detail::overlapped_io_operation ov{}; - WSABUF buf; - buf.buf = buffer.data(); - buf.len = buffer.size(); - DWORD flags = 0, bytes_recv = 0; - - int r = WSARecv(reinterpret_cast(m_socket.native_handle()), &buf, 1, &bytes_recv, &flags, &ov.ov, nullptr); - if (r == 0) // Data already read + static constexpr auto recv_fn = [](SOCKET s, detail::overlapped_io_operation& ov, WSABUF& buf) { - if (bytes_recv == 0) - co_return {read_status::closed, buffer}; - co_return {read_status::ok, std::span{buffer.data(), bytes_recv}}; - } - else if (WSAGetLastError() == WSA_IO_PENDING) - { - auto status = co_await m_io_scheduler->poll(ov.pi, timeout); - if (status == poll_status::event) - { - co_return {read_status::ok, std::span{buffer.data(), ov.bytes_transferred}}; - } - else if (status == poll_status::timeout) - { - BOOL success = CancelIoEx(reinterpret_cast(m_socket.native_handle()), &ov.ov); - if (!success) - { - int err = GetLastError(); - if (err == ERROR_NOT_FOUND) - { - // Operation has been completed - co_return {read_status::ok, std::span{buffer.data(), ov.bytes_transferred}}; - } - } - co_return {read_status::timeout, std::span{}}; - } - } + DWORD flags{}; + return WSARecv(s, &buf, 1, &ov.bytes_transferred, &flags, &ov.ov, nullptr); + }; - co_return {read_status::error, std::span{}}; + co_return co_await detail::perform_write_read_operation, true>( + m_io_scheduler, reinterpret_cast(m_socket.native_handle()), recv_fn, buffer, timeout); } #endif diff --git a/src/net/tcp/server.cpp b/src/net/tcp/server.cpp index e10e2ac2..53bf6006 100644 --- a/src/net/tcp/server.cpp +++ b/src/net/tcp/server.cpp @@ -144,7 +144,8 @@ auto server::accept_client(const std::chrono::milliseconds timeout) -> coro::tas throw std::runtime_error("Failed to retrieve GetAcceptExSockaddrs function pointer"); }); - detail::overlapped_io_operation ovpi{.is_accept = true}; + detail::overlapped_io_operation ovpi{ + .socket = reinterpret_cast(m_accept_socket.native_handle())}; auto client = net::make_socket( socket::options{ From 86a78b565205bb1b2b4936b07e7a94ba3b6f4c36 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Mon, 14 Jul 2025 17:15:45 +0300 Subject: [PATCH 24/24] fix some Codacy problems, remove mutable from signal_win32 --- include/coro/detail/io_notifier_iocp.hpp | 2 +- include/coro/detail/signal_win32.hpp | 4 ++-- include/coro/detail/winsock_handle.hpp | 2 +- src/detail/io_notifier_iocp.cpp | 2 +- src/net/socket.cpp | 4 +++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/include/coro/detail/io_notifier_iocp.hpp b/include/coro/detail/io_notifier_iocp.hpp index 6f279207..4c1758c0 100644 --- a/include/coro/detail/io_notifier_iocp.hpp +++ b/include/coro/detail/io_notifier_iocp.hpp @@ -32,7 +32,7 @@ class io_notifier_iocp auto watch_timer(detail::timer_handle& timer, std::chrono::nanoseconds duration) -> bool; - auto watch(const coro::signal& signal, void* data) -> bool; + auto watch(coro::signal& signal, void* data) -> bool; auto unwatch_timer(detail::timer_handle& timer) -> bool; diff --git a/include/coro/detail/signal_win32.hpp b/include/coro/detail/signal_win32.hpp index cd7c9e5b..ea47e920 100644 --- a/include/coro/detail/signal_win32.hpp +++ b/include/coro/detail/signal_win32.hpp @@ -17,7 +17,7 @@ class signal_win32 void unset(); private: - mutable void* m_iocp{}; - mutable void* m_data{}; + void* m_iocp{}; + void* m_data{}; }; } // namespace coro::detail \ No newline at end of file diff --git a/include/coro/detail/winsock_handle.hpp b/include/coro/detail/winsock_handle.hpp index 76b7bf17..aa888679 100644 --- a/include/coro/detail/winsock_handle.hpp +++ b/include/coro/detail/winsock_handle.hpp @@ -22,7 +22,7 @@ class winsock_handle }; public: - winsock_handle(private_constructor); + explicit winsock_handle(private_constructor); ~winsock_handle(); winsock_handle(const winsock_handle&) = delete; diff --git a/src/detail/io_notifier_iocp.cpp b/src/detail/io_notifier_iocp.cpp index 5386371d..93f49fc1 100644 --- a/src/detail/io_notifier_iocp.cpp +++ b/src/detail/io_notifier_iocp.cpp @@ -80,7 +80,7 @@ auto io_notifier_iocp::unwatch_timer(detail::timer_handle& timer) -> bool return true; } -auto io_notifier_iocp::watch(const coro::signal& signal, void* data) -> bool +auto io_notifier_iocp::watch(coro::signal& signal, void* data) -> bool { signal.m_iocp = m_iocp; signal.m_data = data; diff --git a/src/net/socket.cpp b/src/net/socket.cpp index 82dc8c6f..88145a93 100644 --- a/src/net/socket.cpp +++ b/src/net/socket.cpp @@ -90,7 +90,7 @@ auto socket::shutdown(poll_op how) -> bool break; } return (::shutdown(m_fd, h) == 0); -#elif defined(_WIN32) || defined(_WIN64) +#elif defined(CORO_PLATFORM_WINDOWS) // WinSock uses SD_RECEIVE, SD_SEND, SD_BOTH switch (how) { @@ -106,6 +106,8 @@ auto socket::shutdown(poll_op how) -> bool } return (::shutdown((SOCKET)m_fd, h) == 0); #endif + + (void) h; } auto socket::close() -> void