Skip to content

Commit ccdaf15

Browse files
committed
feat: add returning overload to tcp_acceptor::accept()
Add accept() with no arguments returning io_result<tcp_socket>, so the acceptor constructs the peer socket itself instead of requiring the caller to pass one in: auto [ec, peer] = co_await acc.accept();
1 parent a0d0d1c commit ccdaf15

5 files changed

Lines changed: 264 additions & 1 deletion

File tree

doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,19 @@ connection on `peer` is closed first.
137137
The operation is asynchronous—your coroutine suspends until a connection
138138
arrives or an error occurs.
139139

140+
There is also a returning overload that constructs the peer socket for you,
141+
associated with the acceptor's execution context:
142+
143+
[source,cpp]
144+
----
145+
auto [ec, peer] = co_await acc.accept();
146+
----
147+
148+
Prefer the returning overload for the common case: it is simpler and guarantees
149+
the acceptor and socket use the same `io_context`. Use the
150+
`accept(tcp_socket&)` form when you pre-allocate or recycle sockets and want to
151+
manage their lifetime yourself.
152+
140153
=== Errors
141154

142155
Common accept errors:
@@ -158,7 +171,8 @@ Common accept errors:
158171
=== Preconditions
159172

160173
* The tcp_acceptor must be listening (`is_open() == true`)
161-
* The peer socket must be associated with the same execution context
174+
* For `accept(tcp_socket&)`, the peer socket must be associated with the same
175+
execution context as the acceptor (the returning overload guarantees this)
162176

163177
== Cancellation
164178

include/boost/corosio/native/native_tcp_acceptor.hpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,44 @@ class native_tcp_acceptor : public tcp_acceptor
141141
}
142142
};
143143

144+
struct native_accept_value_awaitable
145+
{
146+
native_tcp_acceptor& acc_;
147+
tcp_socket peer_;
148+
std::stop_token token_;
149+
mutable std::error_code ec_;
150+
mutable io_object::implementation* peer_impl_ = nullptr;
151+
152+
explicit native_accept_value_awaitable(native_tcp_acceptor& acc)
153+
: acc_(acc)
154+
, peer_(acc.context())
155+
{
156+
}
157+
158+
bool await_ready() const noexcept
159+
{
160+
return token_.stop_requested();
161+
}
162+
163+
capy::io_result<tcp_socket> await_resume() noexcept
164+
{
165+
if (token_.stop_requested())
166+
return {make_error_code(std::errc::operation_canceled),
167+
std::move(peer_)};
168+
if (!ec_ && peer_impl_)
169+
acc_.reset_peer_impl(peer_, peer_impl_);
170+
return {ec_, std::move(peer_)};
171+
}
172+
173+
auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
174+
-> std::coroutine_handle<>
175+
{
176+
token_ = env->stop_token;
177+
return acc_.get_impl().accept(
178+
h, env->executor, token_, &ec_, &peer_impl_);
179+
}
180+
};
181+
144182
public:
145183
/** Construct a native acceptor from an execution context.
146184
@@ -208,6 +246,24 @@ class native_tcp_acceptor : public tcp_acceptor
208246
return native_accept_awaitable(*this, peer);
209247
}
210248

249+
/** Asynchronously accept an incoming connection, returning the peer.
250+
251+
Calls the backend implementation directly, bypassing virtual
252+
dispatch. Otherwise identical to @ref tcp_acceptor::accept().
253+
254+
@return An awaitable yielding `io_result<tcp_socket>`.
255+
256+
@throws std::logic_error if the acceptor is not listening.
257+
258+
This acceptor must outlive the returned awaitable.
259+
*/
260+
auto accept()
261+
{
262+
if (!is_open())
263+
detail::throw_logic_error("accept: acceptor not listening");
264+
return native_accept_value_awaitable(*this);
265+
}
266+
211267
/** Asynchronously wait for the acceptor to be ready.
212268
213269
Calls the backend implementation directly, bypassing virtual

include/boost/corosio/tcp_acceptor.hpp

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,45 @@ class BOOST_COROSIO_DECL tcp_acceptor : public io_object
136136
}
137137
};
138138

139+
struct accept_value_awaitable
140+
{
141+
tcp_acceptor& acc_;
142+
tcp_socket peer_;
143+
std::stop_token token_;
144+
mutable std::error_code ec_;
145+
mutable io_object::implementation* peer_impl_ = nullptr;
146+
147+
explicit accept_value_awaitable(tcp_acceptor& acc)
148+
: acc_(acc)
149+
, peer_(acc.context())
150+
{
151+
}
152+
153+
bool await_ready() const noexcept
154+
{
155+
return token_.stop_requested();
156+
}
157+
158+
capy::io_result<tcp_socket> await_resume() noexcept
159+
{
160+
if (token_.stop_requested())
161+
return {make_error_code(std::errc::operation_canceled),
162+
std::move(peer_)};
163+
164+
if (!ec_ && peer_impl_)
165+
peer_.h_.reset(peer_impl_);
166+
return {ec_, std::move(peer_)};
167+
}
168+
169+
auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
170+
-> std::coroutine_handle<>
171+
{
172+
token_ = env->stop_token;
173+
return acc_.get().accept(
174+
h, env->executor, token_, &ec_, &peer_impl_);
175+
}
176+
};
177+
139178
public:
140179
/** Destructor.
141180
@@ -341,6 +380,8 @@ class BOOST_COROSIO_DECL tcp_acceptor : public io_object
341380
// Use peer socket
342381
}
343382
@endcode
383+
384+
@see accept()
344385
*/
345386
auto accept(tcp_socket& peer)
346387
{
@@ -349,6 +390,48 @@ class BOOST_COROSIO_DECL tcp_acceptor : public io_object
349390
return accept_awaitable(*this, peer);
350391
}
351392

393+
/** Initiate an asynchronous accept operation, returning the peer.
394+
395+
Accepts an incoming connection and returns a newly constructed
396+
socket for it, associated with this acceptor's execution context.
397+
The acceptor must be listening before calling this function.
398+
399+
The caller does not pre-construct the peer socket; the returned
400+
socket shares this acceptor's execution context.
401+
402+
The operation supports cancellation via `std::stop_token` through
403+
the affine awaitable protocol. If the associated stop token is
404+
triggered, the operation completes immediately with
405+
`errc::operation_canceled`.
406+
407+
@return An awaitable that completes with `io_result<tcp_socket>`.
408+
On success the payload is the connected peer socket; on failure
409+
(including cancellation) the error code is set and the payload
410+
socket is unconnected. Errors include:
411+
- operation_canceled: Cancelled via stop_token or cancel().
412+
Check `ec == cond::canceled` for portable comparison.
413+
414+
@par Preconditions
415+
The acceptor must be listening (`is_open() == true`). This acceptor
416+
must outlive the returned awaitable.
417+
418+
@par Example
419+
@code
420+
auto [ec, peer] = co_await acc.accept();
421+
if (!ec) {
422+
// peer is a connected socket
423+
}
424+
@endcode
425+
426+
@see accept(tcp_socket&)
427+
*/
428+
auto accept()
429+
{
430+
if (!is_open())
431+
detail::throw_logic_error("accept: acceptor not listening");
432+
return accept_value_awaitable(*this);
433+
}
434+
352435
/** Wait for an incoming connection or readiness condition.
353436
354437
Suspends until the listen socket is ready in the

test/unit/native/native_tcp_acceptor.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ struct native_tcp_acceptor_test
3434
decltype(std::declval<tcp_acceptor&>().accept(
3535
std::declval<tcp_socket&>()))>,
3636
"native_tcp_acceptor::accept must shadow tcp_acceptor::accept");
37+
static_assert(
38+
!std::is_same_v<
39+
decltype(std::declval<native_tcp_acceptor<Backend>&>().accept()),
40+
decltype(std::declval<tcp_acceptor&>().accept())>,
41+
"native_tcp_acceptor::accept() must shadow tcp_acceptor::accept()");
3742
static_assert(
3843
!std::is_same_v<
3944
decltype(std::declval<native_tcp_acceptor<Backend>&>().wait(
@@ -120,6 +125,51 @@ struct native_tcp_acceptor_test
120125
BOOST_TEST(!wait_ec);
121126
}
122127

128+
// Exercise the shadowed returning accept() awaitable on the
129+
// devirtualized path: it yields a connected peer socket.
130+
void testNativeAcceptReturning()
131+
{
132+
io_context ioc(Backend);
133+
auto ex = ioc.get_executor();
134+
135+
native_tcp_acceptor<Backend> acc(ioc);
136+
acc.open();
137+
acc.set_option(native_socket_option::reuse_address(true));
138+
auto bec = acc.bind(endpoint(ipv4_address::loopback(), 0));
139+
BOOST_TEST(!bec);
140+
auto lec = acc.listen();
141+
BOOST_TEST(!lec);
142+
auto port = acc.local_endpoint().port();
143+
144+
native_tcp_socket<Backend> client(ioc);
145+
client.open();
146+
147+
std::error_code accept_ec;
148+
bool accept_done = false;
149+
bool peer_connected = false;
150+
151+
auto acceptor = [&]() -> capy::task<> {
152+
auto [ec, peer] = co_await acc.accept();
153+
accept_ec = ec;
154+
if (!ec)
155+
peer_connected = peer.remote_endpoint().port() != 0;
156+
accept_done = true;
157+
};
158+
auto connector = [&]() -> capy::task<> {
159+
auto [ec] = co_await client.connect(
160+
endpoint(ipv4_address::loopback(), port));
161+
(void)ec;
162+
};
163+
164+
capy::run_async(ex)(acceptor());
165+
capy::run_async(ex)(connector());
166+
ioc.run();
167+
168+
BOOST_TEST(accept_done);
169+
BOOST_TEST(!accept_ec);
170+
BOOST_TEST(peer_connected);
171+
}
172+
123173
#ifdef SO_REUSEPORT
124174
void testNativeReusePort()
125175
{
@@ -143,6 +193,7 @@ struct native_tcp_acceptor_test
143193
testAcceptorMoveConstruct();
144194
testAcceptorPolymorphicSlice();
145195
testWait();
196+
testNativeAcceptReturning();
146197
#ifdef SO_REUSEPORT
147198
testNativeReusePort();
148199
#endif

test/unit/tcp_acceptor.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,64 @@ struct tcp_acceptor_test
287287
acc.close();
288288
}
289289

290+
void testAcceptReturning()
291+
{
292+
// Returning overload: accept() yields the peer socket directly,
293+
// associated with the acceptor's execution context.
294+
io_context ioc(Backend);
295+
tcp_acceptor acc(ioc);
296+
acc.open(tcp::v6());
297+
acc.set_option(socket_option::reuse_address(true));
298+
auto ec = acc.bind(endpoint(ipv6_address::loopback(), 0));
299+
BOOST_TEST(!ec);
300+
ec = acc.listen();
301+
BOOST_TEST(!ec);
302+
auto port = acc.local_endpoint().port();
303+
304+
tcp_socket client(ioc);
305+
306+
bool accept_done = false;
307+
bool connect_done = false;
308+
bool peer_local_v6 = false;
309+
bool peer_remote_v6 = false;
310+
std::error_code accept_ec, connect_ec;
311+
312+
auto ex = ioc.get_executor();
313+
capy::run_async(ex)(
314+
[](tcp_acceptor& a, std::error_code& ec_out, bool& done,
315+
bool& local_v6, bool& remote_v6) -> capy::task<> {
316+
auto [ec, peer] = co_await a.accept();
317+
ec_out = ec;
318+
if (!ec)
319+
{
320+
local_v6 = peer.local_endpoint().is_v6();
321+
remote_v6 = peer.remote_endpoint().is_v6();
322+
}
323+
done = true;
324+
}(acc, accept_ec, accept_done, peer_local_v6, peer_remote_v6));
325+
326+
capy::run_async(ex)(
327+
[](tcp_socket& s, endpoint ep, std::error_code& ec_out,
328+
bool& done) -> capy::task<> {
329+
auto [ec] = co_await s.connect(ep);
330+
ec_out = ec;
331+
done = true;
332+
}(client, endpoint(ipv6_address::loopback(), port), connect_ec,
333+
connect_done));
334+
335+
ioc.run();
336+
337+
BOOST_TEST(accept_done);
338+
BOOST_TEST(!accept_ec);
339+
BOOST_TEST(connect_done);
340+
BOOST_TEST(!connect_ec);
341+
BOOST_TEST(peer_local_v6);
342+
BOOST_TEST(peer_remote_v6);
343+
344+
client.close();
345+
acc.close();
346+
}
347+
290348
void testDualStackAccept()
291349
{
292350
io_context ioc(Backend);
@@ -666,6 +724,7 @@ struct tcp_acceptor_test
666724
// IPv6
667725
testListenV6();
668726
testAcceptV6();
727+
testAcceptReturning();
669728

670729
// Dual-stack
671730
testDualStackAccept();

0 commit comments

Comments
 (0)