Skip to content

Commit

Permalink
Add composed operation examples for c++20.
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskohlhoff committed Jul 5, 2022
1 parent 3292226 commit 64289a9
Show file tree
Hide file tree
Showing 13 changed files with 2,454 additions and 1 deletion.
2 changes: 1 addition & 1 deletion doc/Jamfile.v2
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ local example-names = cpp03/allocation cpp03/buffers cpp03/chat cpp03/echo
cpp11/multicast cpp11/nonblocking cpp11/operations cpp11/socks4 cpp11/ssl
cpp11/timeouts cpp11/timers cpp11/spawn cpp14/deferred cpp14/echo
cpp14/executors cpp14/iostreams cpp14/operations cpp14/parallel_group
cpp17/coroutines_ts cpp20/channels ;
cpp17/coroutines_ts cpp20/channels cpp20/operations ;

for local l in $(example-names)
{
Expand Down
19 changes: 19 additions & 0 deletions doc/examples.qbk
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,25 @@ Example showing how to use a channel in conjunction with C++20 coroutines.
* [@boost_asio/example/cpp20/channels/throttling_proxy.cpp]


[heading Operations]

Examples showing how to implement composed asynchronous operations as reusable library functions.

* [@boost_asio/example/cpp20/operations/composed_1.cpp]
* [@boost_asio/example/cpp20/operations/composed_2.cpp]
* [@boost_asio/example/cpp20/operations/composed_3.cpp]
* [@boost_asio/example/cpp20/operations/composed_4.cpp]
* [@boost_asio/example/cpp20/operations/composed_5.cpp]
* [@boost_asio/example/cpp20/operations/composed_6.cpp]
* [@boost_asio/example/cpp20/operations/composed_7.cpp]
* [@boost_asio/example/cpp20/operations/composed_8.cpp]

Examples showing how to expose callback-based APIs as asynchronous operations.

* [@boost_asio/example/cpp20/operations/callback_wrapper.cpp]
* [@boost_asio/example/cpp20/operations/c_callback_wrapper.cpp]


[endsect]


Expand Down
41 changes: 41 additions & 0 deletions example/cpp20/operations/Jamfile.v2
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#
# Copyright (c) 2003-2022 Christopher M. Kohlhoff (chris at kohlhoff 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)
#

lib socket ; # SOLARIS
lib nsl ; # SOLARIS
lib ws2_32 ; # NT
lib mswsock ; # NT
lib ipv6 ; # HPUX
lib network ; # HAIKU

project
: requirements
<library>/boost/system//boost_system
<library>/boost/chrono//boost_chrono
<define>BOOST_ALL_NO_LIB=1
<threading>multi
<target-os>solaris:<library>socket
<target-os>solaris:<library>nsl
<target-os>windows:<define>_WIN32_WINNT=0x0501
<target-os>windows,<toolset>gcc:<library>ws2_32
<target-os>windows,<toolset>gcc:<library>mswsock
<target-os>windows,<toolset>gcc-cygwin:<define>__USE_W32_SOCKETS
<target-os>hpux,<toolset>gcc:<define>_XOPEN_SOURCE_EXTENDED
<target-os>hpux:<library>ipv6
<target-os>haiku:<library>network
;

exe callback_wrapper : callback_wrapper.cpp ;
exe c_callback_wrapper : c_callback_wrapper.cpp ;
exe composed_1 : composed_1.cpp ;
exe composed_2 : composed_2.cpp ;
exe composed_3 : composed_3.cpp ;
exe composed_4 : composed_4.cpp ;
exe composed_5 : composed_5.cpp ;
exe composed_6 : composed_6.cpp ;
exe composed_7 : composed_7.cpp ;
exe composed_8 : composed_8.cpp ;
232 changes: 232 additions & 0 deletions example/cpp20/operations/c_callback_wrapper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
//
// c_callback_wrapper.cpp
// ~~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2022 Christopher M. Kohlhoff (chris at kohlhoff 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)
//

#include <boost/asio.hpp>
#include <iostream>
#include <memory>
#include <new>

//------------------------------------------------------------------------------

// This is a mock implementation of a C-based API that uses the function pointer
// plus void* context idiom for exposing a callback.

void read_input(const char* prompt, void (*cb)(void*, const char*), void* arg)
{
std::thread(
[prompt = std::string(prompt), cb, arg]
{
std::cout << prompt << ": ";
std::cout.flush();
std::string line;
std::getline(std::cin, line);
cb(arg, line.c_str());
}).detach();
}

//------------------------------------------------------------------------------

// This is an asynchronous operation that wraps the C-based API.

// To map our completion handler into a function pointer / void* callback, we
// need to allocate some state that will live for the duration of the
// operation. A pointer to this state will be passed to the C-based API.
template <boost::asio::completion_handler_for<void(std::string)> Handler>
class read_input_state
{
public:
read_input_state(Handler&& handler)
: handler_(std::move(handler)),
work_(boost::asio::make_work_guard(handler_))
{
}

// Create the state using the handler's associated allocator.
static read_input_state* create(Handler&& handler)
{
// A unique_ptr deleter that is used to destroy uninitialised objects.
struct deleter
{
// Get the handler's associated allocator type. If the handler does not
// specify an associated allocator, we will use a recycling allocator as
// the default. As the associated allocator is a proto-allocator, we must
// rebind it to the correct type before we can use it to allocate objects.
typename std::allocator_traits<
boost::asio::associated_allocator_t<Handler,
boost::asio::recycling_allocator<void>>>::template
rebind_alloc<read_input_state> alloc;

void operator()(read_input_state* ptr)
{
std::allocator_traits<decltype(alloc)>::deallocate(alloc, ptr, 1);
}
} d{boost::asio::get_associated_allocator(handler,
boost::asio::recycling_allocator<void>())};

// Allocate memory for the state.
std::unique_ptr<read_input_state, deleter> uninit_ptr(
std::allocator_traits<decltype(d.alloc)>::allocate(d.alloc, 1), d);

// Construct the state into the newly allocated memory. This might throw.
read_input_state* ptr =
new (uninit_ptr.get()) read_input_state(std::move(handler));

// Release ownership of the memory and return the newly allocated state.
uninit_ptr.release();
return ptr;
}

static void callback(void* arg, const char* result)
{
read_input_state* self = static_cast<read_input_state*>(arg);

// A unique_ptr deleter that is used to destroy initialised objects.
struct deleter
{
// Get the handler's associated allocator type. If the handler does not
// specify an associated allocator, we will use a recycling allocator as
// the default. As the associated allocator is a proto-allocator, we must
// rebind it to the correct type before we can use it to allocate objects.
typename std::allocator_traits<
boost::asio::associated_allocator_t<Handler,
boost::asio::recycling_allocator<void>>>::template
rebind_alloc<read_input_state> alloc;

void operator()(read_input_state* ptr)
{
std::allocator_traits<decltype(alloc)>::destroy(alloc, ptr);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, ptr, 1);
}
} d{boost::asio::get_associated_allocator(self->handler_,
boost::asio::recycling_allocator<void>())};

// To conform to the rules regarding asynchronous operations and memory
// allocation, we must make a copy of the state and deallocate the memory
// before dispatching the completion handler.
std::unique_ptr<read_input_state, deleter> state_ptr(self, d);
read_input_state state(std::move(*self));
state_ptr.reset();

// Dispatch the completion handler through the handler's associated
// executor, using the handler's associated allocator.
boost::asio::dispatch(state.work_.get_executor(),
boost::asio::bind_allocator(d.alloc,
[
handler = std::move(state.handler_),
result = std::string(result)
]() mutable
{
std::move(handler)(result);
}));
}

private:
Handler handler_;

// According to the rules for asynchronous operations, we need to track
// outstanding work against the handler's associated executor until the
// asynchronous operation is complete.
boost::asio::executor_work_guard<
boost::asio::associated_executor_t<Handler>> work_;
};

// The initiating function for the asynchronous operation.
template <boost::asio::completion_token_for<void(std::string)> CompletionToken>
auto async_read_input(const std::string& prompt, CompletionToken&& token)
{
// Define a function object that contains the code to launch the asynchronous
// operation. This is passed the concrete completion handler, followed by any
// additional arguments that were passed through the call to async_initiate.
auto init = [](
boost::asio::completion_handler_for<void(std::string)> auto handler,
const std::string& prompt)
{
// The body of the initiation function object creates the long-lived state
// and passes it to the C-based API, along with the function pointer.
using state_type = read_input_state<decltype(handler)>;
read_input(prompt.c_str(), &state_type::callback,
state_type::create(std::move(handler)));
};

// The async_initiate function is used to transform the supplied completion
// token to the completion handler. When calling this function we explicitly
// specify the completion signature of the operation. We must also return the
// result of the call since the completion token may produce a return value,
// such as a future.
return boost::asio::async_initiate<CompletionToken, void(std::string)>(
init, // First, pass the function object that launches the operation,
token, // then the completion token that will be transformed to a handler,
prompt); // and, finally, any additional arguments to the function object.
}

//------------------------------------------------------------------------------

void test_callback()
{
boost::asio::io_context io_context;

// Test our asynchronous operation using a lambda as a callback. We will use
// an io_context to obtain an associated executor.
async_read_input("Enter your name",
boost::asio::bind_executor(io_context,
[](const std::string& result)
{
std::cout << "Hello " << result << "\n";
}));

io_context.run();
}

//------------------------------------------------------------------------------

void test_deferred()
{
boost::asio::io_context io_context;

// Test our asynchronous operation using the deferred completion token. This
// token causes the operation's initiating function to package up the
// operation with its arguments to return a function object, which may then be
// used to launch the asynchronous operation.
auto op = async_read_input("Enter your name", boost::asio::deferred);

// Launch our asynchronous operation using a lambda as a callback. We will use
// an io_context to obtain an associated executor.
std::move(op)(
boost::asio::bind_executor(io_context,
[](const std::string& result)
{
std::cout << "Hello " << result << "\n";
}));

io_context.run();
}

//------------------------------------------------------------------------------

void test_future()
{
// Test our asynchronous operation using the use_future completion token.
// This token causes the operation's initiating function to return a future,
// which may be used to synchronously wait for the result of the operation.
std::future<std::string> f =
async_read_input("Enter your name", boost::asio::use_future);

std::string result = f.get();
std::cout << "Hello " << result << "\n";
}

//------------------------------------------------------------------------------

int main()
{
test_callback();
test_deferred();
test_future();
}
Loading

0 comments on commit 64289a9

Please sign in to comment.