Skip to content

Commit 7ededab

Browse files
committed
proper handling of immediate completion in IoCP
1 parent e1283aa commit 7ededab

2 files changed

Lines changed: 242 additions & 77 deletions

File tree

‎src/corosio/src/detail/iocp/sockets.cpp‎

Lines changed: 145 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,18 @@ win_socket_impl_internal(win_sockets& svc) noexcept
363363
win_socket_impl_internal::
364364
~win_socket_impl_internal()
365365
{
366+
// Destroy any active initiator coroutines
367+
if (read_initiator_handle_)
368+
read_initiator_handle_.destroy();
369+
if (write_initiator_handle_)
370+
write_initiator_handle_.destroy();
371+
372+
// Free cached frame storage (operator delete in promise_type is no-op)
373+
if (read_initiator_frame_)
374+
::operator delete(read_initiator_frame_);
375+
if (write_initiator_frame_)
376+
::operator delete(write_initiator_frame_);
377+
366378
svc_.unregister_impl(*this);
367379
}
368380

@@ -469,46 +481,31 @@ connect(
469481
}
470482
}
471483

472-
std::coroutine_handle<>
473-
win_socket_impl_internal::
474-
read_some(
475-
capy::coro h,
476-
capy::executor_ref d,
477-
io_buffer_param param,
478-
std::stop_token token,
479-
std::error_code* ec,
480-
std::size_t* bytes_out)
481-
{
482-
// Keep internal alive during I/O
483-
rd_.internal_ptr = shared_from_this();
484+
//------------------------------------------------------------------------------
485+
// Initiator coroutines - receive control via symmetric transfer after caller
486+
// suspends, then initiate the actual I/O.
484487

485-
auto& op = rd_;
486-
op.reset();
487-
op.h = h;
488-
op.d = d;
489-
op.ec_out = ec;
490-
op.bytes_out = bytes_out;
491-
op.start(token);
488+
read_initiator
489+
make_read_initiator(void*& cached, win_socket_impl_internal* impl)
490+
{
491+
impl->do_read_io();
492+
co_return;
493+
}
492494

493-
capy::mutable_buffer bufs[read_op::max_buffers];
494-
op.wsabuf_count = static_cast<DWORD>(
495-
param.copy_to(bufs, read_op::max_buffers));
495+
write_initiator
496+
make_write_initiator(void*& cached, win_socket_impl_internal* impl)
497+
{
498+
impl->do_write_io();
499+
co_return;
500+
}
496501

497-
// Handle empty buffer: complete with 0 bytes via post for consistency
498-
if (op.wsabuf_count == 0)
499-
{
500-
op.bytes_transferred = 0;
501-
op.dwError = 0;
502-
op.empty_buffer = true;
503-
svc_.post(&op);
504-
return std::noop_coroutine();
505-
}
502+
//------------------------------------------------------------------------------
506503

507-
for (DWORD i = 0; i < op.wsabuf_count; ++i)
508-
{
509-
op.wsabufs[i].buf = static_cast<char*>(bufs[i].data());
510-
op.wsabufs[i].len = static_cast<ULONG>(bufs[i].size());
511-
}
504+
void
505+
win_socket_impl_internal::
506+
do_read_io()
507+
{
508+
auto& op = rd_;
512509

513510
op.flags = 0;
514511

@@ -532,7 +529,7 @@ read_some(
532529
svc_.work_finished();
533530
op.dwError = err;
534531
op.complete_immediate();
535-
return std::noop_coroutine();
532+
return;
536533
}
537534
}
538535
else
@@ -555,7 +552,109 @@ read_some(
555552
svc_.post(&op);
556553
}
557554
}
558-
return std::noop_coroutine();
555+
}
556+
557+
void
558+
win_socket_impl_internal::
559+
do_write_io()
560+
{
561+
auto& op = wr_;
562+
563+
svc_.work_started();
564+
565+
int result = ::WSASend(
566+
socket_,
567+
op.wsabufs,
568+
op.wsabuf_count,
569+
nullptr,
570+
0,
571+
&op,
572+
nullptr);
573+
574+
if (result == SOCKET_ERROR)
575+
{
576+
DWORD err = ::WSAGetLastError();
577+
if (err != WSA_IO_PENDING)
578+
{
579+
// Immediate error - must use post(). See do_read_io for explanation.
580+
svc_.work_finished();
581+
op.dwError = err;
582+
svc_.post(&op);
583+
return;
584+
}
585+
}
586+
else
587+
{
588+
// Synchronous completion - use CAS to race with IOCP.
589+
// See do_read_io for detailed explanation.
590+
//
591+
// CRITICAL: Must call work_finished() ONLY if we win the CAS, and must
592+
// not access op after CAS fails. If IOCP wins, it processes the op
593+
// (which may destroy it), so any access to op is use-after-free.
594+
// The IOCP handler calls work_finished() via its work_guard.
595+
if (::InterlockedCompareExchange(&op.ready_, 1, 0) == 0)
596+
{
597+
svc_.work_finished();
598+
op.bytes_transferred = static_cast<DWORD>(op.InternalHigh);
599+
op.dwError = 0;
600+
svc_.post(&op);
601+
}
602+
}
603+
}
604+
605+
//------------------------------------------------------------------------------
606+
607+
std::coroutine_handle<>
608+
win_socket_impl_internal::
609+
read_some(
610+
capy::coro h,
611+
capy::executor_ref d,
612+
io_buffer_param param,
613+
std::stop_token token,
614+
std::error_code* ec,
615+
std::size_t* bytes_out)
616+
{
617+
// Keep internal alive during I/O
618+
rd_.internal_ptr = shared_from_this();
619+
620+
auto& op = rd_;
621+
op.reset();
622+
op.h = h;
623+
op.d = d;
624+
op.ec_out = ec;
625+
op.bytes_out = bytes_out;
626+
op.start(token);
627+
628+
// Prepare buffers (must happen before initiator runs)
629+
capy::mutable_buffer bufs[read_op::max_buffers];
630+
op.wsabuf_count = static_cast<DWORD>(
631+
param.copy_to(bufs, read_op::max_buffers));
632+
633+
// Handle empty buffer: complete with 0 bytes via post for consistency
634+
if (op.wsabuf_count == 0)
635+
{
636+
op.bytes_transferred = 0;
637+
op.dwError = 0;
638+
op.empty_buffer = true;
639+
svc_.post(&op);
640+
return std::noop_coroutine();
641+
}
642+
643+
for (DWORD i = 0; i < op.wsabuf_count; ++i)
644+
{
645+
op.wsabufs[i].buf = static_cast<char*>(bufs[i].data());
646+
op.wsabufs[i].len = static_cast<ULONG>(bufs[i].size());
647+
}
648+
649+
// Destroy previous initiator if any, construct new one into cached frame
650+
if (read_initiator_handle_)
651+
read_initiator_handle_.destroy();
652+
653+
auto initiator = make_read_initiator(read_initiator_frame_, this);
654+
read_initiator_handle_ = initiator.h;
655+
656+
// Symmetric transfer to initiator - I/O starts after caller is suspended
657+
return initiator.h;
559658
}
560659

561660
std::coroutine_handle<>
@@ -579,6 +678,7 @@ write_some(
579678
op.bytes_out = bytes_out;
580679
op.start(token);
581680

681+
// Prepare buffers (must happen before initiator runs)
582682
capy::mutable_buffer bufs[write_op::max_buffers];
583683
op.wsabuf_count = static_cast<DWORD>(
584684
param.copy_to(bufs, write_op::max_buffers));
@@ -598,47 +698,15 @@ write_some(
598698
op.wsabufs[i].len = static_cast<ULONG>(bufs[i].size());
599699
}
600700

601-
svc_.work_started();
701+
// Destroy previous initiator if any, construct new one into cached frame
702+
if (write_initiator_handle_)
703+
write_initiator_handle_.destroy();
602704

603-
int result = ::WSASend(
604-
socket_,
605-
op.wsabufs,
606-
op.wsabuf_count,
607-
nullptr,
608-
0,
609-
&op,
610-
nullptr);
705+
auto initiator = make_write_initiator(write_initiator_frame_, this);
706+
write_initiator_handle_ = initiator.h;
611707

612-
if (result == SOCKET_ERROR)
613-
{
614-
DWORD err = ::WSAGetLastError();
615-
if (err != WSA_IO_PENDING)
616-
{
617-
// Immediate error - must use post(). See read_some for explanation.
618-
svc_.work_finished();
619-
op.dwError = err;
620-
svc_.post(&op);
621-
return std::noop_coroutine();
622-
}
623-
}
624-
else
625-
{
626-
// Synchronous completion - use CAS to race with IOCP.
627-
// See read_some for detailed explanation.
628-
//
629-
// CRITICAL: Must call work_finished() ONLY if we win the CAS, and must
630-
// not access op after CAS fails. If IOCP wins, it processes the op
631-
// (which may destroy it), so any access to op is use-after-free.
632-
// The IOCP handler calls work_finished() via its work_guard.
633-
if (::InterlockedCompareExchange(&op.ready_, 1, 0) == 0)
634-
{
635-
svc_.work_finished();
636-
op.bytes_transferred = static_cast<DWORD>(op.InternalHigh);
637-
op.dwError = 0;
638-
svc_.post(&op);
639-
}
640-
}
641-
return std::noop_coroutine();
708+
// Symmetric transfer to initiator - I/O starts after caller is suspended
709+
return initiator.h;
642710
}
643711

644712
void

‎src/corosio/src/detail/iocp/sockets.hpp‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "src/detail/iocp/mutex.hpp"
2828
#include "src/detail/iocp/wsa_init.hpp"
2929

30+
#include <coroutine>
3031
#include <memory>
3132

3233
#include <MSWSock.h>
@@ -108,6 +109,90 @@ struct accept_op : overlapped_op
108109

109110
//------------------------------------------------------------------------------
110111

112+
/** Initiator coroutine for read operations.
113+
114+
This coroutine receives control via symmetric transfer after the caller
115+
has fully suspended, then initiates the actual I/O. Uses cached frame
116+
allocation to avoid per-operation heap allocations.
117+
*/
118+
struct read_initiator
119+
{
120+
struct promise_type
121+
{
122+
win_socket_impl_internal* impl;
123+
124+
/** Cached allocation - first call allocates, subsequent calls reuse. */
125+
static void* operator new(std::size_t n, void*& cached, win_socket_impl_internal*)
126+
{
127+
if (!cached)
128+
cached = ::operator new(n);
129+
return cached;
130+
}
131+
132+
/** No-op - frame memory freed in socket destructor. */
133+
static void operator delete(void*) noexcept {}
134+
135+
std::suspend_always initial_suspend() noexcept { return {}; }
136+
std::suspend_always final_suspend() noexcept { return {}; }
137+
138+
read_initiator get_return_object()
139+
{
140+
return {std::coroutine_handle<promise_type>::from_promise(*this)};
141+
}
142+
143+
void return_void() {}
144+
void unhandled_exception() { std::terminate(); }
145+
};
146+
147+
using handle_type = std::coroutine_handle<promise_type>;
148+
handle_type h;
149+
};
150+
151+
/** Initiator coroutine for write operations.
152+
153+
This coroutine receives control via symmetric transfer after the caller
154+
has fully suspended, then initiates the actual I/O. Uses cached frame
155+
allocation to avoid per-operation heap allocations.
156+
*/
157+
struct write_initiator
158+
{
159+
struct promise_type
160+
{
161+
win_socket_impl_internal* impl;
162+
163+
/** Cached allocation - first call allocates, subsequent calls reuse. */
164+
static void* operator new(std::size_t n, void*& cached, win_socket_impl_internal*)
165+
{
166+
if (!cached)
167+
cached = ::operator new(n);
168+
return cached;
169+
}
170+
171+
/** No-op - frame memory freed in socket destructor. */
172+
static void operator delete(void*) noexcept {}
173+
174+
std::suspend_always initial_suspend() noexcept { return {}; }
175+
std::suspend_always final_suspend() noexcept { return {}; }
176+
177+
write_initiator get_return_object()
178+
{
179+
return {std::coroutine_handle<promise_type>::from_promise(*this)};
180+
}
181+
182+
void return_void() {}
183+
void unhandled_exception() { std::terminate(); }
184+
};
185+
186+
using handle_type = std::coroutine_handle<promise_type>;
187+
handle_type h;
188+
};
189+
190+
// Coroutine factory functions (defined in sockets.cpp)
191+
read_initiator make_read_initiator(void*& cached, win_socket_impl_internal* impl);
192+
write_initiator make_write_initiator(void*& cached, win_socket_impl_internal* impl);
193+
194+
//------------------------------------------------------------------------------
195+
111196
/** Internal socket state for IOCP-based I/O.
112197
113198
This class contains the actual state for a single socket, including
@@ -132,6 +217,12 @@ class win_socket_impl_internal
132217
write_op wr_;
133218
SOCKET socket_ = INVALID_SOCKET;
134219

220+
// Cached initiator coroutine frames (allocated on first use)
221+
void* read_initiator_frame_ = nullptr;
222+
void* write_initiator_frame_ = nullptr;
223+
read_initiator::handle_type read_initiator_handle_;
224+
write_initiator::handle_type write_initiator_handle_;
225+
135226
public:
136227
explicit win_socket_impl_internal(win_sockets& svc) noexcept;
137228
~win_socket_impl_internal();
@@ -174,6 +265,12 @@ class win_socket_impl_internal
174265
remote_endpoint_ = remote;
175266
}
176267

268+
/** Execute the read I/O operation (called by initiator coroutine). */
269+
void do_read_io();
270+
271+
/** Execute the write I/O operation (called by initiator coroutine). */
272+
void do_write_io();
273+
177274
private:
178275
endpoint local_endpoint_;
179276
endpoint remote_endpoint_;

0 commit comments

Comments
 (0)