-
Notifications
You must be signed in to change notification settings - Fork 86
IOCP support (draft) #359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
IOCP support (draft) #359
Conversation
# Conflicts: # src/net/socket.cpp
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
- 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
…uedCompletionStatus for batched events
Thanks for opening a PR, this will be an awesome addition! I will take a look and give some feedback, but it might take me a week. I will get to it so please give me some time, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool that we get more and more platforms supported here. I really like you're approach here.
#elif defined(CORO_PLATFORM_WINDOWS) | ||
template<typename executor_type> | ||
concept io_executor = executor<executor_type> and requires(executor_type e, coro::detail::poll_info pi, std::chrono::milliseconds timeout) | ||
{ | ||
{ e.poll(pi, timeout) } -> std::same_as<coro::task<poll_status>>; | ||
}; | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont't think that it is a good idea to have platform-dependent definitions of concepts. The main point of concepts is to abstract away those things and to have one unified interface.
With that being said, I would also strongly suggest to keep the interface of the ececutor_type::poll
function the same over all platforms. If necessary we should add some new type that allows use to have the same definition for that function (e.g. use coro::poll_info
instead of the file descriptor and operation on unix systems too as it should contain all the necessary information). The internals of type can differ throughout the platforms but the interfaces should be the same especially for public facing types as I did it with the introduction of kqueue backend. What do you think of that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice idea, it'll be much simpler
|
||
auto watch(fd_t fd, coro::poll_op op, void* data, bool keep = false) -> bool; | ||
|
||
auto watch(const signal& signal, void* data) -> bool; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also needs to be added to io_notifier_kqueue
.
} | ||
#endif | ||
#elif defined(CORO_PLATFORM_WINDOWS) && defined(LIBCORO_FEATURE_NETWORKING) | ||
auto poll(detail::poll_info& pi, std::chrono::milliseconds timeout) -> coro::task<poll_status>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment on the coro::executor
concept (Should be the same interface on all platforms).
#if defined(CORO_PLATFORM_UNIX) | ||
#include <arpa/inet.h> | ||
#include <netdb.h> | ||
#elif defined(CORO_PLATFORM_WINDOWS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The #elif
block is empty so probably unnecessary.
{ | ||
coro::fd_t m_fd; | ||
const void* m_timer_handle_ptr = nullptr; | ||
using native_handle_t = coro::fd_t; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of this we should probably just rename coro::fd_t
to coro::native_handle_t
and the corresponding file fd.hpp
into native_handle.hpp
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➕ I think this makes sense too
|
||
namespace coro::net | ||
{ | ||
enum class read_status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for not using coro::recv_status
? From what I see, they are basically the same, but recv_status gives more detailed error status.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In recv_status/send_status, the values are assigned from <errno.h>. On Windows, you'll have to include the WinAPI headers, which is very undesirable.
You can use a factory (something like make_status)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah I'm wondering how we can unify this, fracturing the two different status' like this seem undesirable from an API usability standpoint
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally agree with you @jbaldwin.
We could include the windows headers only on windows (guarded by the pre-processor) and than define the enum values according to the correct values depending on the platform used?
|
||
namespace coro::net | ||
{ | ||
enum class write_status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for not using coro::send_status
? From what I see, they are basically the same, but send_status gives more detailed error status.
m_iocp, | ||
0, | ||
static_cast<int>(io_notifier::completion_key::signal_set), | ||
(LPOVERLAPPED)m_data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid c-style casts.
m_iocp, | ||
0, | ||
static_cast<int>(io_notifier::completion_key::signal_unset), | ||
(LPOVERLAPPED)m_data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid c-style casts.
m_size.fetch_sub(1, std::memory_order::release); | ||
co_return result; | ||
} | ||
auto io_scheduler::bind_socket(const net::socket& sock) -> void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not experienced with IOCP and windows in general but I don't like that we have one extra step with bind_socket
on windows to use sockets. That is especially bad because it's part of the public facing api of this library. Could you think of some way to get rid of this extra step and embed the body of this function into some already existing place?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Binding a socket in IOCP is mandatory. Accessing the pointer to the IOCP is required to do this. The only way, I think, is to create a getter to io_notifier
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this would be better logically on the net::socket
class and take an io_executor
templated parameter?
I'm not familiar with why CreateIoCompletionPort requires the number of threads but we could expose a io_scheduler::thread_count() -> std::size_t
like function that it could access to get that information.
Getting m_io_notifier.icop()
handle might be more difficult though, its not exactly a platform independent item either.
We could possibly hide this behind a make_socket() -> net::socket
for linux/bsd and make_socket(io_executor&) -> net::socket
for windows, this might be on case where a slightly different API is required per platform.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not familiar with why CreateIoCompletionPort requires the number of threads but we could expose a io_scheduler::thread_count() -> std::size_t like function that it could access to get that information.
I clarified a bit in the documentation, you can just pass 0
The maximum number of threads that the operating system can allow to concurrently process I/O completion packets for the I/O completion port. If this parameter is zero, the system allows as many concurrently running threads as there are processors in the system.
Also from WinAPI docs:
NumberOfConcurrentThreads [in]
The maximum number of threads that the operating system can allow to concurrently process I/O completion packets for the I/O completion port. This parameter is ignored if the ExistingCompletionPort parameter is not NULL.
So there is no need for thread count in make_socket, only IOCP pointer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could possibly hide this behind a make_socket() -> net::socket for linux/bsd and make_socket(io_executor&) -> net::socket for windows, this might be on case where a slightly different API is required per platform.
Also a little about templates, I want to avoid including Windows.h in header files and it's not possible while using templates. We'll have to write something like our own wrapper over WinAPI and WinSock. I think doing it in winsock_handle
is a good idea
Example:
auto make_socket(io_executor auto& executor, const socket::options& opts) -> socket
{
#if defined(CORO_PLATFORM_UNIX)
...
#elif defined(CORO_PLATFORM_WINDOWS)
auto winsock = detail::initialise_winsock();
socket s{
winsock->new_socket(opts.domain, opts.type)
// ::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."};
}
#endif
if (opts.blocking == socket::blocking_t::no)
{
if (s.blocking(socket::blocking_t::no) == false)
{
throw std::runtime_error{"Failed to set socket to non-blocking mode."};
}
}
return s;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this would be better logically on the net::socket class and take an io_executor templated parameter?
I really like this idea! Logically, bind_socket should not reside in the io_scheduler
and to put it into the socket itself seems like a good workaround.
Thank you for feedback, @tglane! I'll take care of your comments when I have time. |
#include "platform.hpp" | ||
|
||
#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) | ||
#if defined(CORO_PLATFORM_BSD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➕
} | ||
} | ||
|
||
inline auto client::write(std::span<const char> buffer, std::chrono::milliseconds timeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think recv/send and read/write need to be unified to work the same for all platforms, I don't think having a split api for windows and everything else is a good way to expose users to the library. If we need to migrate linux/bsd to work like how read/write are here I think that would be OK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to unify read/write, it's impossible to unify recv/send.
Because in Unix and Windows, the order is completely different: on Unix, we first wait for when we can send/recv, and on Windows, we first send/recv, and then wait for the operation to complete.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could get rid of the current implementation of recv/send since they are ultimately only really useable together with poll. The new read/write functions combine those functions so the old ones might not be useful anymore at all?
-> So on unix systems, read/write could just be poll followed by the content of recv/send?
* @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<std::optional<coro::net::tcp::client>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we unify these as possibly:
auto accept(std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) -> coro::task<std::optional<coro::net::tcp::client>>;
?
The linux/bsd versions would need to add timeout support into their respective accept functions since they don't support it right now, but with a unified api I think that should be re-usable
return {static_cast<send_status>(errno), std::span<const char>{}}; | ||
} | ||
} | ||
auto sendto(const info& peer_info, const buffer_type& buffer) -> std::pair<send_status, std::span<const char>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the same question here, how can we unify these functions across all platforms?
@PyXiion I had a chance to give this a once over today, this is very very awesome! Thank you for creating this PR. I think the major item that I'd like to see addressed is how to unify the API between the three platforms, I realize windows is a bit different than linux/bsd but I think we can come up with something that should align the API of this project to be as close as possible for all the platforms as we possibly can. |
@jbaldwin I'm thinking about it too. I suggest using read/write (read_from/write_to for UDP) as a cross-platform option. On Unix, it uses the poll + recv/send functions, while on Windows, it simply works. |
Yeah I'd be good with removing send/recv in favor of read/write if it works on all platforms. |
I'm currently working on removing the recv/send functions and am stuck on the statuses. I'm not sure what the point is in separating them into recv and send statuses (or read and write statuses). It would be simpler to use something like the Like asio does https://think-async.com/Asio/asio-1.30.2/doc/asio/reference/error__basic_errors.html |
I'm currently working on adding Windows support to the networking part of the library.
I've already done tcp::server, tcp::client and udp::peer classes and ran tests on them, added cross-platform methods for them; write/read instead of recv/send.
I need some feedback to improve the PR, I'm not sure about code consistency.
Right now it might not work on Linux systems, because I didn't test in on it, but I'll do, since Linux is my main OS.
Also this is my first contribution, so I'm not sure about commit names (it's kinda messy) and descriptions and other things I probably should know.