ASIO/Networking TS : Boost < 1.70
Thanks to Christos Stratopoulos for this Outcome recipe.
Compatibility note
This recipe targets Boost versions before 1.70, where coroutine support is based around
the asio::experimental::this_coro::token
completion token. For integration with Boost
versions 1.70 and onward, see this recipe.
Use case
Boost.ASIO
and standalone ASIO provide the
async_result
customisation point for adapting arbitrary third party libraries, such as Outcome, into ASIO.
Historically in ASIO you need to pass completion handler instances to the ASIO asynchronous i/o initiation functions. These get executed when the i/o completes.
// Dynamically allocate a buffer to read into. This must be move-only
// so it can be attached to the completion handler, hence the unique_ptr.
auto buffer = std::make_unique<std::vector<byte>>(1024);
// Begin an asynchronous socket read, upon completion invoke
// the lambda function specified
skt.async_read_some(asio::buffer(buffer->data(), buffer->size()),
// Retain lifetime of the i/o buffer until completion
[buffer = std::move(buffer)](const error_code &ec, size_t bytes) {
// Handle the buffer read
if(ec)
{
std::cerr << "Buffer read failed with " << ec << std::endl;
return;
}
std::cout << "Read " << bytes << " bytes into buffer" << std::endl;
// buffer will be dynamically freed now
});
One of the big value adds of the Coroutines TS is the ability to not have to write so much boilerplate if you have a Coroutines supporting compiler:
// As coroutines suspend the calling thread whilst an asynchronous
// operation executes, we can use stack allocation instead of dynamic
// allocation
char buffer[1024];
// Get an ASIO completion token for the current coroutine (requires
// Coroutines TS)
asio::experimental::await_token token = //
co_await asio::experimental::this_coro::token();
// Asynchronously read data, suspending this coroutine until completion,
// returning the bytes of the data read into the result.
try
{
size_t bytesread = //
co_await skt.async_read_some(asio::buffer(buffer), token);
std::cout << "Read " << bytesread << " bytes into buffer" << std::endl;
}
catch(const std::system_error &e)
{
std::cerr << "Buffer read failed with " << e.what() << std::endl;
}
The default ASIO implementation always throws exceptions on failure through
its coroutine token transformation. The redirect_error
token transformation recovers the option to use the error_code
interface,
but it suffers from the same drawbacks
that make pure error codes unappealing in the synchronous case.
This recipe fixes that by making it possible for coroutinised
i/o in ASIO to return a result<T>
:
// Asynchronously read data, suspending this coroutine until completion,
// returning the bytes of the data read into the result, or any failure.
outcome::result<size_t, error_code> bytesread = //
co_await skt.async_read_some(asio::buffer(buffer), as_result(token));
// Usage is exactly like ordinary Outcome. Note the lack of exception throw!
if(bytesread.has_error())
{
std::cerr << "Buffer read failed with " << bytesread.error() << std::endl;
return;
}
std::cout << "Read " << bytesread.value() << " bytes into buffer" << std::endl;
Implementation
The real world, production-level recipe can be found at the bottom of this page. You ought to use that in any real world use case.
It is however worth providing a walkthrough of a simplified edition of the real world recipe, as a lot of barely documented ASIO voodoo is involved. You should not use the code presented next in your own code, it is too simplified. But it should help you understand how the real implementation works.
Firstly we need to define some helper type sugar and a factory function for wrapping any arbitrary third party completion token with that type sugar:
namespace detail
{
// Type sugar for wrapping an external completion token
template <class CompletionToken> struct as_result_t
{
CompletionToken token;
};
} // namespace detail
// Factory function for wrapping a third party completion token with
// our type sugar
template <class CompletionToken> //
inline auto as_result(CompletionToken &&token)
{
return detail::as_result_t<std::decay_t<CompletionToken>>{std::forward<CompletionToken>(token)};
};
Next we tell ASIO about a new completion token it ought to recognise by specialising
async_result
:
// Tell ASIO about a new kind of completion token, the kind returned
// from our as_result() factory function. This implementation is
// for functions with handlers void(error_code, T) only.
template <class CompletionToken, class T> //
struct asio::async_result<detail::as_result_t<CompletionToken>, //
void(error_code, T)> //
// NOTE we subclass for an async result taking an outcome::result
// as its completion handler. We will mangle the void(error_code, T)
// completion handler into this completion handler below.
: public asio::async_result<CompletionToken, void(outcome::result<T, error_code>)>
{
// The result type we shall return
using result_type = outcome::result<T, error_code>;
using _base = asio::async_result<CompletionToken, void(result_type)>;
// The awaitable type to be returned by the initiating function,
// the co_await of which will yield a result_type
using return_type = typename _base::return_type;
// Get what would be the completion handler for the async_result
// whose completion handler is void(result_type)
using result_type_completion_handler_type = //
typename _base::completion_handler_type;
The tricky part to understand is that our async_result
specialisation inherits
from an async_result
for the supplied completion token type with a completion
handler which consumes a result<T>
. Our async_result
is actually therefore
the base async_result
, but we layer on top a completion_handler_type
with
the void(error_code, size_t)
signature which constructs from that a result
:
// Wrap that completion handler with void(error_code, T) converting
// handler
struct completion_handler_type
{
// Pass through unwrapped completion token
template <class U>
completion_handler_type(::detail::as_result_t<U> &&ch)
: _handler(std::forward<U>(ch.token))
{
}
// Our completion handler spec
void operator()(error_code ec, T v)
{
// Call the underlying completion handler, whose
// completion function is void(result_type)
if(ec)
{
// Complete with a failed result
_handler(result_type(outcome::failure(ec)));
return;
}
// Complete with a successful result
_handler(result_type(outcome::success(v)));
}
result_type_completion_handler_type _handler;
};
// Initialise base with the underlying completion handler
async_result(completion_handler_type &h)
: _base(h._handler)
{
}
using _base::get;
};
To use, simply wrap the third party completion token with as_result
to cause
ASIO to return from co_await
a result
instead of throwing exceptions on
failure:
char buffer[1024];
asio::experimental::await_token token =
co_await asio::experimental::this_coro::token();
outcome::result<size_t, error_code> bytesread =
co_await skt.async_read_some(asio::buffer(buffer), as_result(token));
The real world production-level implementation below is a lot more complex than the above which has been deliberately simplified to aid exposition. The above should help you get up and running with the below, eventually.
One again I would like to remind you that Outcome is not the appropriate place to seek help with ASIO voodoo. Please ask on Stackoverflow #boost-asio.
Here follows the real world, production-level adapation of Outcome into ASIO, written and maintained by Christos Stratopoulos. If the following does not load due to Javascript being disabled, you can visit the gist at https://gist.github.com/cstratopoulos/901b5cdd41d07c6ce6d83798b09ecf9b/da584844f58353915dc2600fba959813f793b456.