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();
};