Beast Logo

PrevUpHomeNext

Writing Composed Operations

Asynchronous operations are started by calling a free function or member function known as an asynchronous initiating function. This function accepts parameters specific to the operation as well as a "completion token." The token is either a completion handler, or a type defining how the caller is informed of the asynchronous operation result. Boost.Asio comes with the special tokens boost::asio::use_future and boost::asio::yield_context for using futures and coroutines respectively. This system of customizing the return value and method of completion notification is known as the Extensible Asynchronous Model described in N3747, and a built in to N4588. Here is an example of an initiating function which reads a line from the stream and echoes it back. This function is developed further in the next section:

template<
    class AsyncStream,
    class CompletionToken>
auto
async_echo(AsyncStream& stream, CompletionToken&& token)

Authors using Beast can reuse the library's primitives to create their own initiating functions for performing a series of other, intermediate asynchronous operations before invoking a final completion handler. The set of intermediate actions produced by an initiating function is known as a composed operation. To ensure full interoperability and well-defined behavior, Boost.Asio imposes requirements on the implementation of composed operations. These classes and functions make it easier to develop initiating functions and their composed operations:

Table 9. Asynchronous Helpers

Name

Description

async_completion

This class aggregates the completion handler customization point and the asynchronous initiation function return value customization point into a single object which exposes the appropriate output types for the given input types, and also contains boilerplate that is necessary to implement an initiation function using the Extensible Model.

async_return_type

This template alias determines the return value of an asynchronous initiation function given the completion token and signature. It is used by asynchronous initiation functions to meet the requirements of the Extensible Asynchronous Model.

bind_handler

This function returns a new, nullary completion handler which when invoked with no arguments invokes the original completion handler with a list of bound arguments. The invocation is made from the same implicit or explicit strand as that which would be used to invoke the original handler. This is accomplished by using the correct overload of asio_handler_invoke associated with the original completion handler.

handler_alloc

This class meets the requirements of Allocator, and uses any custom memory allocation and deallocation hooks associated with a given handler. It is useful for when a composed operation requires temporary dynamic allocations to achieve its result. Memory allocated using this allocator must be freed before the final completion handler is invoked.

handler_ptr

This is a smart pointer container used to manage the internal state of a composed operation. It is useful when the state is non trivial. For example when the state has non-copyable or expensive to copy types. The container takes ownership of the final completion handler, and provides boilerplate to invoke the final handler in a way that also deletes the internal state. The internal state is allocated using the final completion handler's associated allocator, benefiting from all handler memory management optimizations transparently.

handler_type

This template alias converts a completion token and signature to the correct completion handler type. It is used in the implementation of asynchronous initiation functions to meet the requirements of the Extensible Asynchronous Model.


This example develops an initiating function called echo. The operation will read up to the first newline on a stream, and then write the same line including the newline back on the stream. The implementation performs both reading and writing, and has a non-trivially-copyable state. First we define the input parameters and results, then declare our initiation function. For our echo operation the only inputs are the stream and the completion token. The output is the error code which is usually included in all completion handler signatures.

/** Asynchronously read a line and echo it back.

    This function is used to asynchronously read a line ending
    in a carriage-return ("CR") from the stream, and then write
    it back. The function call always returns immediately. The
    asynchronous operation will continue until one of the
    following conditions is true:

    @li A line was read in and sent back on the stream

    @li An error occurs.

    This operation is implemented in terms of one or more calls to
    the stream's `async_read_some` and `async_write_some` functions,
    and is known as a <em>composed operation</em>. The program must
    ensure that the stream performs no other operations until this
    operation completes. The implementation may read additional octets
    that lie past the end of the line being read. These octets are
    silently discarded.

    @param The stream to operate on. The type must meet the
    requirements of @b AsyncReadStream and @AsyncWriteStream

    @param token The completion token to use. If this is a
    completion handler, copies will be made as required.
    The equivalent signature of the handler must be:
    @code
    void handler(
        error_code ec       // result of operation
    );
    @endcode
    Regardless of whether the asynchronous operation completes
    immediately or not, the handler will not be invoked from within
    this function. Invocation of the handler will be performed in a
    manner equivalent to using `boost::asio::io_service::post`.
*/
template<
    class AsyncStream,
    class CompletionToken>
beast::async_return_type<       1
    CompletionToken,
    void(beast::error_code)>    2
async_echo(
    AsyncStream& stream,
    CompletionToken&& token);

1

The async_return_type customizes the return value based on the completion token

2

This is the signature for the completion handler

Now that we have a declaration, we will define the body of the function. We want to achieve the following goals: perform static type checking on the input parameters, set up the return value as per N3747, and launch the composed operation by constructing the object and invoking it.

template<class AsyncStream, class Handler>
class echo_op;

// Read a line and echo it back
//
template<class AsyncStream, class CompletionToken>
beast::async_return_type<CompletionToken, void(beast::error_code)>
async_echo(AsyncStream& stream, CompletionToken&& token)
{
    // Make sure stream meets the requirements. We use static_assert
    // to cause a friendly message instead of an error novel.
    //
    static_assert(beast::is_async_stream<AsyncStream>::value,
        "AsyncStream requirements not met");

    // This helper manages some of the handler's lifetime and
    // uses the result and handler specializations associated with
    // the completion token to help customize the return value.
    //
    beast::async_completion<CompletionToken, void(beast::error_code)> init{token};

    // Create the composed operation and launch it. This is a constructor
    // call followed by invocation of operator(). We use handler_type
    // to convert the completion token into the correct handler type,
    // allowing user-defined specializations of the async_result template
    // to be used.
    //
    echo_op<AsyncStream, beast::handler_type<CompletionToken, void(beast::error_code)>>{
        stream, init.completion_handler}(beast::error_code{}, 0);

    // This hook lets the caller see a return value when appropriate.
    // For example this might return std::future<error_code> if
    // CompletionToken is boost::asio::use_future, or this might
    // return an error code if CompletionToken specifies a coroutine.
    //
    return init.result.get();
}

The initiating function contains a few relatively simple parts. There is the customization of the return value type, static type checking, building the return value type using the helper, and creating and launching the composed operation object. The echo_op object does most of the work here, and has a somewhat non-trivial structure. This structure is necessary to meet the stringent requirements of composed operations (described in more detail in the Boost.Asio documentation). We will touch on these requirements without explaining them in depth.

Here is the boilerplate present in all composed operations written in this style:

// This composed operation reads a line of input and echoes it back.
//
template<class AsyncStream, class Handler>
class echo_op
{
    // This holds all of the state information required by the operation.
    struct state
    {
        // The stream to read and write to
        AsyncStream& stream;

        // Indicates what step in the operation's state machine
        // to perform next, starting from zero.
        int step = 0;

        // The buffer used to hold the input and output data.
        //
        // We use a custom allocator for performance, this allows
        // the implementation of the io_service to make efficient
        // re-use of memory allocated by composed operations during
        // a continuation.
        //
        boost::asio::basic_streambuf<beast::handler_alloc<char, Handler>> buffer;

        // handler_ptr requires that the first parameter to the
        // contained object constructor is a reference to the
        // managed final completion handler.
        //
        explicit state(Handler& handler, AsyncStream& stream_)
            : stream(stream_)
            , buffer((std::numeric_limits<std::size_t>::max)(),
                beast::handler_alloc<char, Handler>{handler})
        {
        }
    };

    // The operation's data is kept in a cheap-to-copy smart
    // pointer container called `handler_ptr`. This efficiently
    // satisfies the CopyConstructible requirements of completion
    // handlers.
    //
    // `handler_ptr` uses these memory allocation hooks associated
    // with the final completion handler, in order to allocate the
    // storage for `state`:
    //
    //      asio_handler_allocate
    //      asio_handler_deallocate
    //
    beast::handler_ptr<state, Handler> p_;

public:
    // Boost.Asio requires that handlers are CopyConstructible.
    // In some cases, it takes advantage of handlers that are
    // MoveConstructible. This operation supports both.
    //
    echo_op(echo_op&&) = default;
    echo_op(echo_op const&) = default;

    // The constructor simply creates our state variables in
    // the smart pointer container.
    //
    template<class DeducedHandler, class... Args>
    echo_op(AsyncStream& stream, DeducedHandler&& handler)
        : p_(std::forward<DeducedHandler>(handler), stream)
    {
    }

    // The entry point for this handler. This will get called
    // as our intermediate operations complete. Definition below.
    //
    void operator()(beast::error_code ec, std::size_t bytes_transferred);

    // The next four functions are required for our class
    // to meet the requirements for composed operations.
    // Definitions and exposition will follow.

    template<class AsyncStream_, class Handler_, class Function>
    friend void  asio_handler_invoke(
        Function&& f, echo_op<AsyncStream_, Handler_>* op);

    template<class AsyncStream_, class Handler_>
    friend void* asio_handler_allocate(
        std::size_t size, echo_op<AsyncStream_, Handler_>* op);

    template<class AsyncStream_, class Handler_>
    friend void  asio_handler_deallocate(
        void* p, std::size_t size, echo_op<AsyncStream_, Handler_>* op);

    template<class AsyncStream_, class Handler_>
    friend bool  asio_handler_is_continuation(
        echo_op<AsyncStream_, Handler_>* op);
};

Next is to implement the function call operator. Our strategy is to make our composed object meet the requirements of a completion handler by being copyable (also movable), and by providing the function call operator with the correct signature. Rather than using std::bind or boost::bind, which destroys the type information and therefore breaks the allocation and invocation hooks, we will simply pass std::move(*this) as the completion handler parameter for any operations that we initiate. For the move to work correctly, care must be taken to ensure that no access to data members are made after the move takes place. Here is the implementation of the function call operator for this echo operation:

// echo_op is callable with the signature void(error_code, bytes_transferred),
// allowing `*this` to be used as both a ReadHandler and a WriteHandler.
//
template<class AsyncStream, class Handler>
void echo_op<AsyncStream, Handler>::
operator()(beast::error_code ec, std::size_t bytes_transferred)
{
    // Store a reference to our state. The address of the state won't
    // change, and this solves the problem where dereferencing the
    // data member is undefined after a move.
    auto& p = *p_;

    // Now perform the next step in the state machine
    switch(ec ? 2 : p.step)
    {
        // initial entry
        case 0:
            // read up to the first newline
            p.step = 1;
            return boost::asio::async_read_until(p.stream, p.buffer, "\r", std::move(*this));

        case 1:
            // write everything back
            p.step = 2;
            // async_read_until could have read past the newline,
            // use buffer_prefix to make sure we only send one line
            return boost::asio::async_write(p.stream,
                beast::buffer_prefix(bytes_transferred, p.buffer.data()), std::move(*this));

        case 2:
            p.buffer.consume(bytes_transferred);
            break;
    }

    // Invoke the final handler. The implementation of `handler_ptr`
    // will deallocate the storage for the state before the handler
    // is invoked. This is necessary to provide the
    // destroy-before-invocation guarantee on handler memory
    // customizations.
    //
    // If we wanted to pass any arguments to the handler which come
    // from the `state`, they would have to be moved to the stack
    // first or else undefined behavior results.
    //
    p_.invoke(ec);
    return;
}

This is the most important element of writing a composed operation, and the part which is often neglected or implemented incorrectly. It is the declaration and definition of the "handler hooks". There are four hooks:

Table 10. Handler Hooks

Name

Description

asio_handler_invoke

Default invoke function for handlers. This hooking function ensures that the invoked method used for the final handler is accessible at each intermediate step.

asio_handler_allocate

Default allocation function for handlers. Implement asio_handler_allocate and asio_handler_deallocate for your own handlers to provide custom allocation for temporary objects.

asio_handler_deallocate

Default deallocation function for handlers. Implement asio_handler_allocate and asio_handler_deallocate for your own handlers to provide custom allocation for temporary objects.

asio_handler_is_continuation

Default continuation function for handlers. Implement asio_handler_is_continuation for your own handlers to indicate when a handler represents a continuation.


Our composed operation stores the final handler and performs its own intermediate asynchronous operations. To ensure that I/O objects, in this case the stream, are accessed safely it is important to use the same method to invoke intermediate handlers as that used to invoke the final handler. Similarly, for the memory allocation hooks our composed operation should use the same hooks as those used by the final handler. And finally for the asio_is_continuation hook, we want to return true for any intermediate asynchronous operations we perform after the first one, since those represent continuations. For the first asynchronous operation we perform, the hook should return true only if the final handler also represents a continuation. Our implementation of the hooks will forward the call to the corresponding overloads of the final handler:

// Handler hook forwarding. These free functions invoke the hooks
// associated with the final completion handler. In effect, they
// make the Asio implementation treat our composed operation the
// same way it would treat the final completion handler for the
// purpose of memory allocation and invocation.
//
// Our implementation just passes the call through to the hook
// associated with the final handler. The "using" statements are
// structured to permit argument dependent lookup. Always use
// `std::addressof` or its equivalent to pass the pointer to the
// handler, otherwise an unwanted overload of `operator&` may be
// called instead.

template<class AsyncStream, class Handler, class Function>
void asio_handler_invoke(
    Function&& f, echo_op<AsyncStream, Handler>* op)
{
    using boost::asio::asio_handler_invoke;
    return asio_handler_invoke(f, std::addressof(op->p_.handler()));
}

template<class AsyncStream, class Handler>
void* asio_handler_allocate(
    std::size_t size, echo_op<AsyncStream, Handler>* op)
{
    using boost::asio::asio_handler_allocate;
    return asio_handler_allocate(size, std::addressof(op->p_.handler()));
}

template<class AsyncStream, class Handler>
void asio_handler_deallocate(
    void* p, std::size_t size, echo_op<AsyncStream, Handler>* op)
{
    using boost::asio::asio_handler_deallocate;
    return asio_handler_deallocate(p, size,
        std::addressof(op->p_.handler()));
}

// Determines if the next asynchronous operation represents a
// continuation of the asynchronous flow of control associated
// with the final handler. If we are past step one, it means
// we have performed an asynchronous operation therefore any
// subsequent operation would represent a continuation.
// Otherwise, we propagate the handler's associated value of
// is_continuation. Getting this right means the implementation
// may schedule the invokation of the invoked functions more
// efficiently.
//
template<class AsyncStream, class Handler>
bool asio_handler_is_continuation(echo_op<AsyncStream, Handler>* op)
{
    // This next call is structured to permit argument
    // dependent lookup to take effect.
    using boost::asio::asio_handler_is_continuation;

    // Always use std::addressof to pass the pointer to the handler,
    // otherwise an unwanted overload of operator& may be called instead.
    return op->p_->step > 1 ||
        asio_handler_is_continuation(std::addressof(op->p_.handler()));
}

There are some common mistakes that should be avoided when writing composed operations:

  • Type erasing the final handler. This will cause undefined behavior.
  • Not using std::addressof to get the address of the handler.
  • Forgetting to include a return statement after calling an initiating function.
  • Calling a synchronous function by accident. In general composed operations should not block for long periods of time, since this ties up a thread running on the io_service.
  • Forgetting to overload asio_handler_invoke for the composed operation. This will cause undefined behavior if someone calls the initiating function with a strand-wrapped function object, and there is more than thread running on the io_service.
  • For operations which complete immediately (i.e. without calling an intermediate initiating function), forgetting to use io_service::post to invoke the final handler. This breaks the following initiating function guarantee: Regardless of whether the asynchronous operation completes immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using boost::asio::io_service::post. The function bind_handler is provided for this purpose.

A complete, runnable version of this example may be found in the examples directory.


PrevUpHomeNext