In short, wrapping a handler and asio_handler_invoke
accomplish two different tasks:
- wrap a handler to customize the invocation of a handler.
- define
asio_handler_invoke
hook to customize the invocation of other handlers in the context of a handler.
template <typename Handler>
struct custom_handler
{
void operator()(...); // Customize invocation of handler_.
Handler handler_;
};
// Customize invocation of Function within context of custom_handler.
template <typename Function>
void asio_handler_invoke(Function function, custom_handler* context);
// Invoke custom invocation of 'perform' within context of custom_handler.
void perform() {...}
custom_handler handler;
using boost::asio::asio_handler_invoke;
asio_handler_invoke(std::bind(&perform), &handler);
The primary reason for the asio_handler_invoke
hook is to allow one to customize the invocation strategy of handlers to which the application may not have direct access. For instance, consider composed operations that are composed of zero or more calls to intermediate operations. For each intermediate operation, an intermediate handler will be created on behalf of the application, but the application does not have direct access to these handlers. When using custom handlers, the asio_handler_invoke
hook provides a way to customize the invocation strategy of these intermediate handlers within a given context. The documentation states:
When asynchronous operations are composed from other asynchronous operations, all intermediate handlers should be invoked using the same method as the final handler. This is required to ensure that user-defined objects are not accessed in a way that may violate the guarantees. This [asio_handler_invoke
] hooking function ensures that the invoked method used for the final handler is accessible at each intermediate step.
asio_handler_invoke
Consider a case where we wish to count the number of asynchronous operations executed, including each intermediate operation in composed operations. To do this, we need to create a custom handler type, counting_handler
, and count the number of times functions are invoked within its context:
template <typename Handler>
class counting_handler
{
void operator()(...)
{
// invoke handler
}
Handler handler_;
};
template <typename Function>
void asio_handler_invoke(Function function, counting_handler* context)
{
// increment counter
// invoke function
}
counting_handler handler(&handle_read);
boost::asio::async_read(socket, buffer, handler);
In the above snippet, the function handle_read
is wrapped by counting_handler
. As the counting_handler
is not interested in counting the number of times the wrapped handler is invoked, its operator()
will not increment the count and just invoke handle_read
. However, the counting_handler
is interested in the amount of handlers invoked within its context in the async_read
operation, so the custom invocation strategy in asio_handler_invoke
will increment a count.
Example
Here is a concrete example based on the above counting_handler
type. The operation_counter
class provides a way to easily wrap application handlers with a counting_handler
:
namespace detail {
/// @brief counting_handler is a handler that counts the number of
/// times a handler is invoked within its context.
template <class Handler>
class counting_handler
{
public:
counting_handler(Handler handler, std::size_t& count)
: handler_(handler),
count_(count)
{}
template <class... Args>
void operator()(Args&&... args)
{
handler_(std::forward<Args>(args)...);
}
template <typename Function>
friend void asio_handler_invoke(
Function intermediate_handler,
counting_handler* my_handler)
{
++my_handler->count_;
// Support chaining custom strategies incase the wrapped handler
// has a custom strategy of its own.
using boost::asio::asio_handler_invoke;
asio_handler_invoke(intermediate_handler, &my_handler->handler_);
}
private:
Handler handler_;
std::size_t& count_;
};
} // namespace detail
/// @brief Auxiliary class used to wrap handlers that will count
/// the number of functions invoked in their context.
class operation_counter
{
public:
template <class Handler>
detail::counting_handler<Handler> wrap(Handler handler)
{
return detail::counting_handler<Handler>(handler, count_);
}
std::size_t count() { return count_; }
private:
std::size_t count_ = 0;
};
...
operation_counter counter;
boost::asio::async_read(socket, buffer, counter.wrap(&handle_read));
io_service.run();
std::cout << "Count of async_read_some operations: " <<
counter.count() << std::endl;
The async_read()
composed operation will be implemented in zero or more intermediate stream.async_read_some()
operations. For each of these intermediate operations, a handler with an unspecified type will be created and invoked. If the above async_read()
operation was implemented in terms of 2
intermediate async_read_some()
operations, then counter.count()
will be 2
, and the handler returned from counter.wrap()
got invoked once.
On the other hand, if one were to not provide an asio_handler_invoke
hook and instead only incremented the count within the wrapped handler's invocation, then the count would be 1
, reflecting only the count of times the wrapped handler was invoked:
template <class Handler>
class counting_handler
{
public:
...
template <class... Args>
void operator()(Args&&... args)
{
++count_;
handler_(std::forward<Args>(args)...);
}
// No asio_handler_invoke implemented.
};
Here is a complete example demonstrating counting the number of asynchronous operations that get executed, including intermediate operations from a composed operation. The example only initiates three async operations (async_accept
, async_connect
, and async_read
), but the async_read
operation will be composed of 2
intermediate async_read_some
operations:
#include <functional> // std::bind
#include <iostream> // std::cout, std::endl
#include <utility> // std::forward
#include <boost/asio.hpp>
// This example is not interested in the handlers, so provide a noop function
// that will be passed to bind to meet the handler concept requirements.
void noop() {}
namespace detail {
/// @brief counting_handler is a handler that counts the number of
/// times a handler is invoked within its context.
template <class Handler>
class counting_handler
{
public:
counting_handler(Handler handler, std::size_t& count)
: handler_(handler),
count_(count)
{}
template <class... Args>
void operator()(Args&&... args)
{
handler_(std::forward<Args>(args)...);
}
template <typename Function>
friend void asio_handler_invoke(
Function function,
counting_handler* context)
{
++context->count_;
// Support chaining custom strategies incase the wrapped handler
// has a custom strategy of its own.
using boost::asio::asio_handler_invoke;
asio_handler_invoke(function, &context->handler_);
}
private:
Handler handler_;
std::size_t& count_;
};
} // namespace detail
/// @brief Auxiliary class used to wrap handlers that will count
/// the number of functions invoked in their context.
class operation_counter
{
public:
template <class Handler>
detail::counting_handler<Handler> wrap(Handler handler)
{
return detail::counting_handler<Handler>(handler, count_);
}
std::size_t count() { return count_; }
private:
std::size_t count_ = 0;
};
int main()
{
using boost::asio::ip::tcp;
operation_counter all_operations;
// Create all I/O objects.
boost::asio::io_service io_service;
tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));
tcp::socket socket1(io_service);
tcp::socket socket2(io_service);
// Connect the sockets.
// operation 1: acceptor.async_accept
acceptor.async_accept(socket1, all_operations.wrap(std::bind(&noop)));
// operation 2: socket2.async_connect
socket2.async_connect(acceptor.local_endpoint(),
all_operations.wrap(std::bind(&noop)));
io_service.run();
io_service.reset();
// socket1 and socket2 are connected. The scenario below will:
// - write bytes to socket1.
// - initiate a composed async_read operaiton to read more bytes
// than are currently available on socket2. This will cause
// the operation to complete with multple async_read_some
// operations on socket2.
// - write more bytes to socket1.
// Write to socket1.
std::string write_buffer = "demo";
boost::asio::write(socket1, boost::asio::buffer(write_buffer));
// Guarantee socket2 has received the data.
assert(socket2.available() == write_buffer.size());
// Initiate a composed operation to more data than is immediately
// available. As some data is available, an intermediate async_read_some
// operation (operation 3) will be executed, and another async_read_some
// operation (operation 4) will eventually be initiated.
std::vector<char> read_buffer(socket2.available() + 1);
operation_counter read_only;
boost::asio::async_read(socket2, boost::asio::buffer(read_buffer),
all_operations.wrap(read_only.wrap(std::bind(&noop))));
// Write more to socket1. This will cause the async_read operation
// to be complete.
boost::asio::write(socket1, boost::asio::buffer(write_buffer));
io_service.run();
std::cout << "total operations: " << all_operations.count() << "
"
"read operations: " << read_only.count() << std::endl;
}
Output:
total operations: 4
read operations: 2
Composed Handlers
In the above example, the async_read()
handler was composed of a handler wrapped twice. First by the operation_counter
that is only counting for read operations, and then the resulting functor was wrapped by the operation_counter
counting all operations:
<pre class="lang-cp