Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

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 Networking.TS. 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 1.8. Asynchronous Helpers

Name

Description

bind_handler

This function creates a new handler which, when invoked, calls the original handler with the list of bound arguments. Any parameters passed in the invocation will be substituted for placeholders present in the list of bound arguments. Parameters which are not matched to placeholders are silently discarded.

The passed handler and arguments are forwarded into the returned handler, whose associated allocator and associated executor will will be the same as those of the original handler.

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-movable or contains expensive to move 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.


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_context::post`.
*/
template<
    class AsyncStream,
    class CompletionToken>
BOOST_ASIO_INITFN_RESULT_TYPE(          1
    CompletionToken,
    void(boost::beast::error_code))     2
async_echo(
    AsyncStream& stream,
    CompletionToken&& token);

1

BOOST_ASIO_INITFN_RESULT_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>
BOOST_ASIO_INITFN_RESULT_TYPE(CompletionToken, void(boost::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(boost::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.
    //
    boost::asio::async_completion<CompletionToken, void(boost::beast::error_code)> init{token};

    // Create the composed operation and launch it. This is a constructor
    // call followed by invocation of operator(). We use BOOST_ASIO_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,
        BOOST_ASIO_HANDLER_TYPE(
            CompletionToken, void(boost::beast::error_code))>{
                stream,
                init.completion_handler}(boost::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;

        // Boost.Asio and the Networking TS require an object of
        // type executor_work_guard<T>, where T is the type of
        // executor returned by the stream's get_executor function,
        // to persist for the duration of the asynchronous operation.
        boost::asio::executor_work_guard<
            decltype(std::declval<AsyncStream&>().get_executor())> work;

        // 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_context to make efficient
        // re-use of memory allocated by composed operations during
        // a continuation.
        //
        boost::asio::basic_streambuf<typename std::allocator_traits<
            boost::asio::associated_allocator_t<Handler> >::
                template rebind_alloc<char> > 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 const& handler, AsyncStream& stream_)
            : stream(stream_)
            , work(stream.get_executor())
            , buffer((std::numeric_limits<std::size_t>::max)(),
                boost::asio::get_associated_allocator(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 with expensive-to-copy state.
    //
    // `handler_ptr` uses the allocator associated with the final
    // completion handler, in order to allocate the storage for `state`.
    //
    boost::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)
    {
    }

    // Associated allocator support. This is Asio's system for
    // allowing the final completion handler to customize the
    // memory allocation strategy used for composed operation
    // states. A composed operation should use the same allocator
    // as the final handler. These declarations achieve that.

    using allocator_type =
        boost::asio::associated_allocator_t<Handler>;

    allocator_type
    get_allocator() const noexcept
    {
        return (boost::asio::get_associated_allocator)(p_.handler());
    }

    // Executor hook. This is Asio's system for customizing the
    // manner in which asynchronous completion handlers are invoked.
    // A composed operation needs to use the same executor to invoke
    // intermediate completion handlers as that used to invoke the
    // final handler.

    using executor_type = boost::asio::associated_executor_t<
        Handler, decltype(std::declval<AsyncStream&>().get_executor())>;

    executor_type get_executor() const noexcept
    {
        return (boost::asio::get_associated_executor)(
            p_.handler(), p_->stream.get_executor());
    }

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

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 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()(boost::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 buffers_prefix to make sure we only send one line
            return boost::asio::async_write(p.stream,
                boost::beast::buffers_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.
    //
    // The work guard is moved to the stack first, otherwise it would
    // be destroyed before the handler is invoked.
    //
    auto work = std::move(p.work);
    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 forwarding of the final handler's associated allocator and associated executor to the composed operation.

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 executor to invoke intermediate handlers as that used to invoke the final handler. Similarly, for memory allocations our composed operation should use the allocator associated with the final handler.

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

  • Type erasing the final handler. This will cause undefined behavior.
  • 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 boost::asio::io_context.
  • Forgetting to provide executor_type and get_executor for the composed operation. This will cause undefined behavior. For example, if someone calls the initiating function with a strand-wrapped function object, and there is more than thread running on the boost::asio::io_context, the underlying stream may be accessed in a fashion that violates safety guarantees.
  • Forgetting to create an object of type boost::asio::executor_work_guard with the type of executor returned by the stream's get_executor member function.
  • For operations which complete immediately (i.e. without calling an intermediate initiating function), forgetting to use boost::asio::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::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