I/O runtimes: Asio and Corosio

A build of the library is on one of two I/O runtimes, chosen when it is configured:

  • ASIO, on Asio: every operation is an asynchronous operation of Asio, and takes any completion token. It is also there as a coroutine.

  • COROSIO, on Corosio: coroutines only.

Each brings the sockets, timers and TLS, on top of an async runtime: the executors, coroutines and cancellation. Asio is both in one. Corosio is the I/O only, on top of Capy, which is the async runtime.

Both build the same library and protocol sources. Only a thin runtime layer differs, anyhttp/runtime.hpp and anyhttp/net.hpp with what they include from asio/ or corosio/. docs/corosio-port-plan.md has how that came about.

That layer is the inline namespace anyhttp::asio or anyhttp::corosio. Its names are anyhttp’s, Task or errors::eof, and a library of one runtime does not link with code compiled for the other. Because asio and corosio are names in anyhttp, code that says using namespace anyhttp writes Boost’s namespaces in full, as in boost::asio::buffer(): a namespace alias asio of its own would be ambiguous.

What the names stand for

ASIO

COROSIO

CMake

-DANYHTTP_API=ASIO (default)

-DANYHTTP_API=COROSIO -DTLS_LIBRARY=OpenSSL

Task<T>

boost::asio::awaitable<T>

boost::capy::task<T>

Executor

boost::asio::any_io_executor

boost::capy::any_executor

error_code

boost::system::error_code

std::error_code

results<Ts...>

std::tuple<error_code, Ts...>

boost::capy::results<Ts...>

IoTask<Ts...>

boost::asio::awaitable<results<Ts...>>

boost::capy::task<results<Ts...>>

TcpEndpoint, UdpEndpoint

boost::asio::ip::tcp::endpoint, boost::asio::ip::udp::endpoint

boost::corosio::endpoint

Operations

async_x(..., token) and x()

x()

TLS

AWS-LC (default) or OpenSSL 3.5+

OpenSSL 3.5+

Programs

server, client

server

Buffers are Asio’s on both, headers are Beast’s Fields, URLs Boost.URL’s.

The reference shows what the headers have for ASIO, as Doxygen is told to read them that way. So the reference has the operations with completion tokens that a COROSIO build does not have, and the types as they are in the table. corosio/runtime.hpp has the same names as asio/runtime.hpp, in the same order.

The shape of an operation

On ASIO, an operation comes in two shapes. The asynchronous operation of Asio is named async_... and takes a completion token last, which decides what it returns, and the coroutine of the plain name wraps it. On COROSIO, the coroutine is all there is. Taking Reader::read_some() for an example, with the types of the library:

// ASIO: the operation, which completes a handler of the signature void(error_code, size_t) ...
template <typename CompletionToken = DefaultCompletionToken>
auto async_read_some(boost::asio::mutable_buffer buffer, CompletionToken&& token = CompletionToken());

// ... and the coroutine around it.
IoTask<size_t> read_some(boost::asio::mutable_buffer buffer);

// COROSIO: the coroutine.
IoTask<size_t> read_some(boost::asio::mutable_buffer buffer);

The coroutine has the same declaration on both, so code that uses only coroutines builds with either, which is what the guide does. It yields results, a tuple with the error first, and reports what goes wrong instead of throwing it; IoTask<size_t> is short for Task<results<size_t>>. Every operation has it: read_some, write, write_eof, submit, get_response, connect, get, and the others. What it does not have is a completion token, so an ASIO build that wants one, for a callback, for cancel_after or to have errors thrown, uses the async_... shape, see Completion tokens in the guide.

With the types spelled out, the same piece of code on ASIO, using both shapes:

boost::asio::awaitable<std::size_t> read_twice(Reader& reader, boost::asio::mutable_buffer buffer)
{
   // The asynchronous operation, with a completion token that reports errors instead of throwing.
   auto [ec, n] = co_await reader.async_read_some(buffer, boost::asio::as_tuple);
   if (ec)
      co_return n;

   // The coroutine around it.
   boost::asio::awaitable<std::tuple<boost::system::error_code, std::size_t>> next =
      reader.read_some(buffer + n);
   auto [next_ec, m] = co_await std::move(next);
   co_return n + m;
}

And on COROSIO:

boost::capy::task<std::size_t> read_twice(Reader& reader, boost::asio::mutable_buffer buffer)
{
   // The coroutine, which is all there is.
   auto [ec, n] = co_await reader.read_some(buffer);
   if (ec)
      co_return n;

   boost::capy::task<std::tuple<std::error_code, std::size_t>> next = reader.read_some(buffer + n);
   auto [next_ec, m] = co_await std::move(next);
   co_return n + m;
}

On ASIO, the implementation of each operation is hidden behind any_completion_handler, so that it can be compiled separately. On COROSIO, they are coroutines of Capy, Corosio’s base, cancelled through the awaiting chain’s std::stop_token. Reader and Writer model boost::capy::ReadStream and boost::capy::WriteStream.

What the errors are

Where the two runtimes spell an error differently, the namespace anyhttp::errors names it:

ASIO

COROSIO

errors::eof

boost::asio::error::eof

boost::capy::error::eof

errors::partial_message

beast::http::error::partial_message

boost::capy::error::stream_truncated

errors::canceled

errc::operation_canceled

boost::capy::error::canceled

errors::would_block

boost::asio::error::would_block

errc::operation_would_block

errors::connection_aborted

boost::asio::error::connection_aborted

errc::connection_aborted

errors::header_limit

beast::http::error::header_limit

the same

Anything else compares against errc:: (boost::system::errc or std::errc), with one exception: a host that does not resolve is netdb_errors::host_not_found on ASIO and errc::no_such_device_or_address on COROSIO.