Tutorial

Prerequisites

This tutorial assumes that you are familiar with Boost.Asio and asynchronous programming using callbacks.

Goal

We are going to use Boost.Asio to build a function that will print out the resource at an http url with the headers

Our function will have this signature

template <class Finished>
auto print_url(boost::asio::io_service &io, std::string host, std::string url,
               Finished f) {

Coroutine variables

We need some way of persisting variables across invocations of async functions. To do this we will define a structure to hold these variables.

  struct coroutine_variables_t {
    boost::asio::ip::tcp::resolver resolver;
    boost::asio::ip::tcp::socket socket;
    boost::asio::streambuf request;
    std::string current;
    explicit coroutine_variables_t(boost::asio::io_service &io)
        : resolver{io}, socket{io} {}
  };

Coroutine block

The logic in a coroutine is defined by the block (or compound statement), which is a sequence of lambdas (or function objects). We open our block like this

  static auto block = stackless_coroutine::make_block(

First lambda

We now define the first lambda of the block

      [](auto &context, auto &variables, const std::string &host,
         const std::string &path) {
        std::ostream request_stream(&variables.request);

        request_stream << "GET " << path << " HTTP/1.0\r\n";
        request_stream << "Host: " << host << "\r\n";
        request_stream << "Accept: */*\r\n";
        request_stream << "Connection: close\r\n\r\n";

        boost::asio::ip::tcp::resolver::query query{host, "http"};

        // Pass context in to any function that requires a callback
        variables.resolver.async_resolve(query, context);

        // Return do_async to signal that we have made an async call, and that
        // we should not go on the next
        // lamda upon return
        return context.do_async();
      },

All lambdas in a block take auto& context, auto& variables. Some lamdas also take other parameters. The first lamda in a coroutine can take additional parameters that must be passed when invoking the coroutine. In this case it takes const std::string &host, const std::string &path. These are used to construct the request stream that will be used later.

Notice how we access the members of coroutine_variables_t using variables

We then set up the query object and call async_resolve, passing in context for the callback.

The last statement

        return context.do_async();

Tells stackless_coroutine that an async operation has been performed, and that, the coroutine should be suspended.

Handling callbacks

In the previous section, we saw how to pass context as a callback

        return context.do_async();

In this section, we will see how see how we access the results of the callback.

Here is the next lambda

      [](auto &context, auto &variables, auto ec, auto iterator) {

        // We can throw exceptions, and the exception will exit the entire block
        if (ec) {
          throw boost::system::system_error{ec};
        } else {

          // Pass context as the callback
          boost::asio::async_connect(variables.socket, iterator, context);
          return context.do_async();
        }

      },

As mentioned earlier, lamdas can other parameters in addition to auto &context, auto &variables. When a lambda follows a lambda that returns context.do_async(), it takes as parameters the results of the callback, which in this case are the error code and iterator.

We examine the error_code ec, and if it is set, we throw an exception. Note that the exception will exit the coroutine.

If we had no errors, we call async_connect passing in the iterator, and context as the callback.

We return context.do_async() signaling stackless_coroutine to suspend the coroutine.

Sending the http request

We use async_write to send the http request

      [](auto &context, auto &variables, auto &ec, auto iterator) {

        if (ec) {
          throw boost::system::system_error{ec};
        }

        // Send the request.
        boost::asio::async_write(variables.socket, variables.request, context);
        return context.do_async();
      },

We then check for errors and throw an exception if there is an error

      [](auto &context, auto &variables, auto &ec, auto n) {
        if (ec) {
          throw boost::system::system_error{ec};
        }

      },

Looping

Writing loops is one of the harder parts of async programming with callbacks. Stackless_coroutines makes this much easier. In this session we will see see how to do loops.

To create a loop, where you would use a lambda, you use make_while_true like this.

      stackless_coroutine::make_while_true(

Then as arguments for make_while_true you pass in a sequence of lambdas. These lambdas will be executed repeatedly until one of the following happens to exit the loop

  • Call to context.do_break
  • Call to context.do_async_break

A loop can also be exited by one of the following which will exit the whole coroutine

  • Call to context.do_return
  • Call to context.do_async_return
  • An exception escapes a lamda

Let us now pass in the first lambda to make_while_true

          [](auto &context, auto &variables) {
            variables.current.resize(20);
            variables.socket.async_read_some(
                boost::asio::buffer(&variables.current[0],
                                    variables.current.size()),
                context);
            return context.do_async();

          },

This lambda, apart from from being inside make_while_true is just like the other other coroutine lambdas.

First, we resize variables.current to have size 20. In real life we would set the buffer size to much bigger. For this example, we made it small to make it more likely that we run this loop multiple times.

We make the call to async_read_some just like the other async calls in the coroutine, passing in context as the callback, and returning context.do_async from the lambda.

In the next lambda we receive the results of async_read_some`

          [](auto &context, auto &variables, auto &ec, auto n) {
            if (ec) {
              if (ec != boost::asio::error::eof)
                std::cerr << ec.message();
              // context.do_break breaks out of the loop
              return context.do_break();
            } else {

              variables.current.resize(n);
              std::cout << variables.current;

              // context.do_continue continues the loop out of the loop
              // We could also return do_next() which will continue to the next
              // lamda
              // which in this case would result in re-entering the loop at the
              // top
              return context.do_continue();
            }

          }

          ));

We check if there is a an error. If there is an error, if it is not eof, we output the error. We also call context.do_break to break the loop.

If there is not an error, we call context.do_continue continue the loop

With this we are finished with our call to make_while_true and make_block

Creating the coroutine

To create the coroutine we call make_coroutine

  auto co = stackless_coroutine::make_coroutine<coroutine_variables_t>(
      block, std::move(f), io);

make_coroutine takes as a template parameter the type of the coroutine variables. It also takes the block, and the function to call upon completion of the coroutine. It also takes, arguments to the coroutine variables type’s constructor.

Calling the coroutine

We call the coroutine co passing in the arguments to the first lambda of the block - host and path

We then return from the print_url function

  return co(host, url);
}

int main()

Here is our main function that uses print_url

int main() {

  boost::asio::io_service io;
  print_url(
      io, "www.httpbin.org", "/",

      // The callback - which takes a reference to the coroutine variables
      // struct, an exception pointer, and op which tells us how we exited the
      // coroutine
      [&](auto &variables, std::exception_ptr e, auto op) {
        if (e) {
          std::cerr << "\nHad an exception\n";
        } else {
          std::cout << "\nFinished successfully\n";
        }
      });

  io.run();
};

Pay particular attention to the lambda we pass to print_url as that is the callback function for the coroutine

      [&](auto &variables, std::exception_ptr e, auto op) {
        if (e) {
          std::cerr << "\nHad an exception\n";
        } else {
          std::cout << "\nFinished successfully\n";
        }
      });

The callback function takes a reference to the coroutine variables, an std::exception_ptr containing any exception that escaped any of the coroutine block lambdas, and stackless_coroutine::operation which tells whether the coroutine exited via an early return. You can usually ignore this last parameter

Complete example.cpp

// Copyright 2016 John R. Bandela
// 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)

#include "stackless_coroutine.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <iostream>

template <class Finished>
auto print_url(boost::asio::io_service &io, std::string host, std::string url,
               Finished f) {

  // Holds values that must be across calls to the lambdas
  struct coroutine_variables_t {
    boost::asio::ip::tcp::resolver resolver;
    boost::asio::ip::tcp::socket socket;
    boost::asio::streambuf request;
    std::string current;
    explicit coroutine_variables_t(boost::asio::io_service &io)
        : resolver{io}, socket{io} {}
  };
  // Create the block for the coroutine
  static auto block = stackless_coroutine::make_block(

      // For the first lamda of the block, in addition to context, and
      // variables,
      // can take other parameters
      [](auto &context, auto &variables, const std::string &host,
         const std::string &path) {
        std::ostream request_stream(&variables.request);

        request_stream << "GET " << path << " HTTP/1.0\r\n";
        request_stream << "Host: " << host << "\r\n";
        request_stream << "Accept: */*\r\n";
        request_stream << "Connection: close\r\n\r\n";

        boost::asio::ip::tcp::resolver::query query{host, "http"};

        // Pass context in to any function that requires a callback
        variables.resolver.async_resolve(query, context);

        // Return do_async to signal that we have made an async call, and that
        // we should not go on the next
        // lamda upon return
        return context.do_async();
      },
      // In the next lambda after a do_async, in addition to context, and
      // variables,
      // also take the parameters
      // of the callback, in this case the error_code ec, and iterator
      [](auto &context, auto &variables, auto ec, auto iterator) {

        // We can throw exceptions, and the exception will exit the entire block
        if (ec) {
          throw boost::system::system_error{ec};
        } else {

          // Pass context as the callback
          boost::asio::async_connect(variables.socket, iterator, context);
          return context.do_async();
        }

      },
      // Take the parameters for the async_connect callback
      [](auto &context, auto &variables, auto &ec, auto iterator) {

        if (ec) {
          throw boost::system::system_error{ec};
        }

        // Send the request.
        boost::asio::async_write(variables.socket, variables.request, context);
        return context.do_async();
      },
      [](auto &context, auto &variables, auto &ec, auto n) {
        if (ec) {
          throw boost::system::system_error{ec};
        }

      },

      // make_while_true creates a loop, that always repeats, unless
      // context.do_break is called
      stackless_coroutine::make_while_true(
          [](auto &context, auto &variables) {
            variables.current.resize(20);
            variables.socket.async_read_some(
                boost::asio::buffer(&variables.current[0],
                                    variables.current.size()),
                context);
            return context.do_async();

          },
          [](auto &context, auto &variables, auto &ec, auto n) {
            if (ec) {
              if (ec != boost::asio::error::eof)
                std::cerr << ec.message();
              // context.do_break breaks out of the loop
              return context.do_break();
            } else {

              variables.current.resize(n);
              std::cout << variables.current;

              // context.do_continue continues the loop out of the loop
              // We could also return do_next() which will continue to the next
              // lamda
              // which in this case would result in re-entering the loop at the
              // top
              return context.do_continue();
            }

          }

          ));

  // pass the block, and the callback, along with any arguments for our
  // cororoutine_variables_t
  // constructor,
  // which in this case is io to make_coroutine
  auto co = stackless_coroutine::make_coroutine<coroutine_variables_t>(
      block, std::move(f), io);

  // call co with arguments corresponding to the parameters of the first lambda
  // after context and variables
  // which in this case is host, and url
  return co(host, url);
}


int main() {

  boost::asio::io_service io;
  print_url(
      io, "www.httpbin.org", "/",

      // The callback - which takes a reference to the coroutine variables
      // struct, an exception pointer, and op which tells us how we exited the
      // coroutine
      [&](auto &variables, std::exception_ptr e, auto op) {
        if (e) {
          std::cerr << "\nHad an exception\n";
        } else {
          std::cout << "\nFinished successfully\n";
        }
      });

  io.run();
};