Using a Capy Stream from Asio
Drive a Capy any_stream with Asio completion tokens, here plain callbacks.
|
|
What This Example Shows
-
An adapter,
uni_stream, that gives aany_streamthe Asio universal asynchronous model withnet::async_initiate -
Each operation starting a Capy task with
run_async, on an executor that posts through Asio -
net::async_writeandnet::async_readcomposed over the adapter -
Two callback chains that share one Asio
io_context
Source Code
//
// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/capy
//
#include "api/uni_stream.hpp"
#include <boost/asio/read.hpp>
#include <boost/asio/write.hpp>
#include <cstdio>
#include <cstring>
#include <memory>
constexpr std::size_t total_bytes = 1024;
struct do_write
{
struct state
{
uni_stream& stream;
std::size_t total;
std::size_t written = 0;
char buf[128];
state(uni_stream& s, std::size_t t)
: stream(s)
, total(t)
{
std::memset(buf, 'X', sizeof(buf));
}
};
std::shared_ptr<state> state_;
do_write(uni_stream& stream, std::size_t total)
: state_(std::make_shared<state>(stream, total))
{
}
void operator()()
{
if(state_->written >= state_->total)
{
std::printf("writer: done, wrote %zu bytes\n", state_->written);
return;
}
std::size_t chunk = (std::min)(sizeof(state_->buf), state_->total - state_->written);
net::async_write(
state_->stream,
net::buffer(state_->buf, chunk),
[self = *this](boost::system::error_code ec, std::size_t n) mutable
{
if(ec)
{
std::printf("writer error: %s\n", ec.message().c_str());
return;
}
self.state_->written += n;
std::printf("writer: wrote %zu bytes (total %zu)\n", n, self.state_->written);
self();
});
}
};
struct do_read
{
struct state
{
uni_stream& stream;
std::size_t total;
std::size_t read_total = 0;
char buf[128];
state(uni_stream& s, std::size_t t)
: stream(s)
, total(t)
{
}
};
std::shared_ptr<state> state_;
do_read(uni_stream& stream, std::size_t total)
: state_(std::make_shared<state>(stream, total))
{
}
void operator()()
{
if(state_->read_total >= state_->total)
{
std::printf("reader: done, read %zu bytes\n", state_->read_total);
std::printf("example complete!\n");
return;
}
std::size_t remaining = state_->total - state_->read_total;
std::size_t to_read = (std::min)(sizeof(state_->buf), remaining);
net::async_read(
state_->stream,
net::buffer(state_->buf, to_read),
[self = *this](boost::system::error_code ec, std::size_t n) mutable
{
if(ec)
{
std::printf("reader error: %s\n", ec.message().c_str());
return;
}
self.state_->read_total += n;
std::printf("reader: read %zu bytes (total %zu)\n", n, self.state_->read_total);
self();
});
}
};
int main()
{
// Type-erased asio::io_context
asio_context ctx;
// Return two connected uni_stream
auto [client, server] = make_uni_pair(ctx);
// Start the writer (callback chain)
do_write(client, total_bytes)();
// Start the reader (callback chain)
do_read(server, total_bytes)();
// Run the I/O context, this returns when all work is done
ctx.run();
return 0;
}
api/uni_stream.hpp
//
// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/capy
//
#ifndef CAPY_EXAMPLE_UNI_STREAM_HPP
#define CAPY_EXAMPLE_UNI_STREAM_HPP
#include <boost/asio/associated_executor.hpp>
#include <boost/asio/async_result.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/post.hpp>
#include "abandon_guard.hpp"
#include "capy_streams.hpp"
#include <boost/capy/buffers/asio.hpp>
#include <boost/capy/io/any_stream.hpp>
#include <boost/capy/ex/frame_allocator.hpp>
#include <boost/capy/ex/run_async.hpp>
#include <boost/capy/task.hpp>
#include <utility>
namespace detail {
template<class AsioExecutor>
class asio_executor_wrapper
{
AsioExecutor ex_;
asio_context* ctx_;
public:
asio_executor_wrapper(
AsioExecutor ex,
asio_context* ctx) noexcept
: ex_(std::move(ex))
, ctx_(ctx)
{
}
bool operator==(asio_executor_wrapper const& other) const noexcept
{
return ex_ == other.ex_;
}
asio_context& context() const noexcept
{
return *ctx_;
}
void on_work_started() const noexcept {}
void on_work_finished() const noexcept {}
std::coroutine_handle<> dispatch(capy::continuation& c) const
{
post(c);
return std::noop_coroutine();
}
void post(capy::continuation& c) const
{
resume_or_abandon r(c);
try
{
net::post(ex_, std::move(r));
}
catch(...)
{
// A handler Asio took and then destroyed has given `c` up,
// so the post must not also report it as still held.
if(r.release())
throw;
}
}
};
} // namespace detail
/** Asio-compatible stream wrapper for capy::any_stream.
This class wraps a capy::any_stream and provides async_read_some
and async_write_some member functions that conform to Asio's
Universal Async Model, accepting completion tokens.
@par Example
@code
asio_context ctx;
auto [s1, s2] = make_stream_pair(ctx);
uni_stream stream(std::move(s1), ctx);
// Use with completion handler
stream.async_read_some(net::buffer(data, size),
[](boost::system::error_code ec, std::size_t n) {
// handle completion
});
// Use with use_awaitable
auto [ec, n] = co_await stream.async_read_some(
net::buffer(data, size), net::as_tuple(net::use_awaitable));
@endcode
*/
class uni_stream
{
capy::any_stream stream_;
asio_context* ctx_;
class initiate_async_read_some
{
uni_stream* self_;
public:
explicit initiate_async_read_some(uni_stream* self) noexcept
: self_(self)
{
}
template<class ReadHandler, class MutableBufferSequence>
void operator()(
ReadHandler&& handler,
MutableBufferSequence const& buffers) const
{
auto ex = net::get_associated_executor(
handler, self_->ctx_->context().get_executor());
detail::asio_executor_wrapper wrapper(std::move(ex), self_->ctx_);
capy::run_async(wrapper)(
[](capy::any_stream* stream,
MutableBufferSequence bufs,
ReadHandler h) -> capy::task<>
{
auto [ec, n] = co_await stream->read_some(
capy::from_asio(bufs));
std::move(h)(ec, n);
}(&self_->stream_, buffers, std::forward<ReadHandler>(handler)));
}
};
class initiate_async_write_some
{
uni_stream* self_;
public:
explicit initiate_async_write_some(uni_stream* self) noexcept
: self_(self)
{
}
template<class WriteHandler, class ConstBufferSequence>
void operator()(
WriteHandler&& handler,
ConstBufferSequence const& buffers) const
{
auto ex = net::get_associated_executor(
handler, self_->ctx_->context().get_executor());
detail::asio_executor_wrapper wrapper(std::move(ex), self_->ctx_);
capy::run_async(wrapper)(
[](capy::any_stream* stream,
ConstBufferSequence bufs,
WriteHandler h) -> capy::task<>
{
auto [ec, n] = co_await stream->write_some(
capy::from_asio(bufs));
std::move(h)(ec, n);
}(&self_->stream_, buffers, std::forward<WriteHandler>(handler)));
}
};
public:
/// The executor type associated with the stream.
using executor_type = net::io_context::executor_type;
/** Constructor.
@param stream The any_stream to wrap (moved).
@param ctx The asio_context for executor operations.
*/
uni_stream(
capy::any_stream stream,
asio_context& ctx) noexcept
: stream_(std::move(stream))
, ctx_(&ctx)
{
}
uni_stream(uni_stream const&) = delete;
uni_stream& operator=(uni_stream const&) = delete;
uni_stream(uni_stream&&) = default;
uni_stream& operator=(uni_stream&&) = default;
/** Returns the executor associated with the stream.
*/
executor_type get_executor() const noexcept
{
return ctx_->context().get_executor();
}
/** Start an asynchronous read operation.
This function reads data into the provided buffer sequence.
The operation completes when at least one byte has been read,
or an error occurs.
@param buffers The buffer sequence to read into.
@param token The completion token. The completion handler
signature is `void(boost::system::error_code, std::size_t)`.
@return The return value depends on the completion token type.
*/
template<class MutableBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(
void(boost::system::error_code, std::size_t)) ReadToken>
auto
async_read_some(
MutableBufferSequence const& buffers,
ReadToken&& token)
{
return net::async_initiate<ReadToken,
void(boost::system::error_code, std::size_t)>(
initiate_async_read_some(this),
token,
buffers);
}
/** Start an asynchronous write operation.
This function writes data from the provided buffer sequence.
The operation completes when at least one byte has been written,
or an error occurs.
@param buffers The buffer sequence to write from.
@param token The completion token. The completion handler
signature is `void(boost::system::error_code, std::size_t)`.
@return The return value depends on the completion token type.
*/
template<class ConstBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(
void(boost::system::error_code, std::size_t)) WriteToken>
auto
async_write_some(
ConstBufferSequence const& buffers,
WriteToken&& token)
{
return net::async_initiate<WriteToken,
void(boost::system::error_code, std::size_t)>(
initiate_async_write_some(this),
token,
buffers);
}
};
/** Create a connected pair of uni_stream.
@param ctx The asio_context for both streams.
@return A pair of connected uni_stream objects.
*/
inline
std::pair<uni_stream, uni_stream>
make_uni_pair(asio_context& ctx)
{
auto [s1, s2] = make_stream_pair(ctx);
return {
uni_stream(std::move(s1), ctx),
uni_stream(std::move(s2), ctx)
};
}
#endif
Build
add_executable(capy_example_asio_callbacks
asio_callbacks.cpp
api/capy_streams.cpp)
target_link_libraries(capy_example_asio_callbacks
Boost::capy
Boost::asio)
b2 builds the same program as exe asio_callbacks from example/asio/Jamfile.
Walkthrough
The Initiation
uni_stream holds a any_stream and a pointer to the asio_context.
Its async_read_some passes three things to net::async_initiate: an initiate_async_read_some object, the completion token, and the buffers.
The completion token decides what async_read_some returns.
With a plain callback, it returns nothing.
Asio then calls initiate_async_read_some with the completion handler and the buffers.
The initiation looks up the executor associated with the completion handler.
The default is the executor of the io_context.
The initiation wraps that executor in asio_executor_wrapper, a Capy executor that posts every continuation through Asio.
Then it starts a task with run_async on the wrapper.
The task converts the buffers with capy::from_asio, awaits read_some on the any_stream, and calls the completion handler with the error code and the byte count.
The wrapper posts even when Capy asks it to dispatch.
So the completion handler never runs inside the call to async_read_some, and it runs on the associated executor, as Asio requires.
async_write_some and initiate_async_write_some follow the same pattern for writes.
A Callback Chain
do_write keeps its state in a std::shared_ptr, so every copy of the function object shares it.
Each call writes one chunk of up to 128 bytes with net::async_write.
That composed operation calls async_write_some on the adapter until the whole chunk is written.
The completion handler of each write adds the bytes written, prints the total, and calls a copy of do_write again.
The chain ends when it has written 1024 bytes.
do_read is the same chain for net::async_read.
main makes a connected pair of adapters with make_uni_pair, starts both chains, and calls ctx.run().
The call returns when both chains end and no work remains.
Output
The two chains run over a local TCP connection, so their lines can interleave differently from run to run.
writer: wrote 128 bytes (total 128)
reader: read 128 bytes (total 128)
writer: wrote 128 bytes (total 256)
reader: read 128 bytes (total 256)
writer: wrote 128 bytes (total 384)
reader: read 128 bytes (total 384)
writer: wrote 128 bytes (total 512)
reader: read 128 bytes (total 512)
writer: wrote 128 bytes (total 640)
reader: read 128 bytes (total 640)
writer: wrote 128 bytes (total 768)
reader: read 128 bytes (total 768)
writer: wrote 128 bytes (total 896)
reader: read 128 bytes (total 896)
writer: wrote 128 bytes (total 1024)
writer: done, wrote 1024 bytes
reader: read 128 bytes (total 1024)
reader: done, read 1024 bytes
example complete!
Exercises
-
Rewrite the reader as an Asio coroutine with
net::co_spawn, using thenet::use_awaitableform that the comment onuni_streamshows. -
Bind the write handler to an Asio strand with
net::bind_executor, and confirm that the completion handler runs on that strand.
See Also
-
Calling Asio from a Capy Coroutine — the other direction, Asio operations awaited from a Capy coroutine
-
The Dimovian Ideal —
any_stream.cpp, stream algorithms written with no Asio headers -
Bridging a Foreign Awaitable — the general pattern for connecting another library