[/ Copyright (c) 2013-2017 Vinnie Falco (vinnie dot falco at gmail dot 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) ] [section:tutorial Writing Composed Operations] To illustrate the usage of the asynchronous helpers in the core section of this library, we will develop a simple asynchronous composed operation called [*echo]. This operation will read up to the first newline on a stream, and then write the same line including the newline back on the stream. 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. ``` #include #include #include #include #include // Read a line and echo it back // template beast::async_return_type async_echo(AsyncStream& stream, CompletionToken&& token) ``` 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 echo_op; // This is our composed operation implementation // Read a line and echo it back // template beast::async_return_type 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::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 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 take effect. // echo_op>{ 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 if // CompletionToken is boost::asio::use_future, or this might // return an error code if CompletionToken specifies a coroutine. // return init.result.get(); } ``` The initiation 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 __Asio__ documentation). We will touch on these requirements without explaining them in depth. First we will create boilerplate which is present in all composed operations written in this style: ``` // This composed operation reads a line of input and echoes it back. // template 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. // Note that 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 continuations. // boost::asio::basic_streambuf> 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::max)(), beast::handler_alloc{handler}) { } }; // This smart pointer container allocates our state using the // memory allocation hooks associated with the final completion // handler, manages the lifetime of that handler for us, and // enforces the destroy-before-invocation requirement on memory // allocated using the hooks. // beast::handler_ptr 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 echo_op(AsyncStream& stream, DeducedHandler&& handler) : p_(std::forward(handler), stream) { } // 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. // friend bool asio_handler_is_continuation(echo_op* 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())); } // 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 through the call to the hook // associated with the final handler. friend void* asio_handler_allocate(std::size_t size, echo_op* op) { using boost::asio::asio_handler_allocate; return asio_handler_allocate(size, std::addressof(op->p_.handler())); } friend void asio_handler_deallocate(void* p, std::size_t size, echo_op* op) { using boost::asio::asio_handler_deallocate; return asio_handler_deallocate(p, size, std::addressof(op->p_.handler())); } template friend void asio_handler_invoke(Function&& f, echo_op* op) { using boost::asio::asio_handler_invoke; return asio_handler_invoke(f, std::addressof(op->p_.handler())); } // Our main entry point. This will get called as our // intermediate operations complete. Definition below. // void operator()(beast::error_code ec, std::size_t bytes_transferred); }; ``` We have the common boilerplate for a composed operation and now we just need 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: ``` // We are callable with the signature void(error_code, bytes_transferred), // allowing `*this` to be used as both a ReadHandler and a WriteHandler. // template void echo_op:: 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, "\n", 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. If we wanted to pass any arguments // which come from our state, they would have to be moved to the // stack first, since the `handler_ptr` guarantees that the state // is destroyed before the handler is invoked. // p_.invoke(ec); return; } ``` A complete, runnable version of this example may be found in the examples directory. [endsect]