diff --git a/include/continuable/detail/external/asio.hpp b/include/continuable/detail/external/asio.hpp index 33f1721..8a901cf 100644 --- a/include/continuable/detail/external/asio.hpp +++ b/include/continuable/detail/external/asio.hpp @@ -30,62 +30,63 @@ #ifndef CONTINUABLE_DETAIL_ASIO_HPP_INCLUDED #define CONTINUABLE_DETAIL_ASIO_HPP_INCLUDED +#include #include #include #include #include #if defined(ASIO_STANDALONE) -#include -#include -#include -#include +# include +# include +# include +# include -#if defined(CONTINUABLE_HAS_EXCEPTIONS) -#include -#endif +# if defined(CONTINUABLE_HAS_EXCEPTIONS) +# include +# endif -#if (ASIO_VERSION < 101300) // 1.13.0 -#define CTI_DETAIL_ASIO_HAS_NO_INTEGRATION -#elif (ASIO_VERSION < 101600) // 1.16.0 (boost 1.72 baseline) -#define CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION -#endif +# if (ASIO_VERSION < 101300) // 1.13.0 +# define CTI_DETAIL_ASIO_HAS_NO_INTEGRATION +# elif (ASIO_VERSION < 101600) // 1.16.0 (boost 1.72 baseline) +# define CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION +# endif -#define CTI_DETAIL_ASIO_NAMESPACE_BEGIN namespace asio { -#define CTI_DETAIL_ASIO_NAMESPACE_END } +# define CTI_DETAIL_ASIO_NAMESPACE_BEGIN namespace asio { +# define CTI_DETAIL_ASIO_NAMESPACE_END } #else -#include -#include -#include -#include +# include +# include +# include +# include -#if defined(CONTINUABLE_HAS_EXCEPTIONS) -#include -#endif +# if defined(CONTINUABLE_HAS_EXCEPTIONS) +# include +# endif -#if (BOOST_VERSION < 107000) // 1.70 -#define CTI_DETAIL_ASIO_HAS_NO_INTEGRATION -#elif (BOOST_VERSION < 107200) // 1.72 -#define CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION -#endif +# if (BOOST_VERSION < 107000) // 1.70 +# define CTI_DETAIL_ASIO_HAS_NO_INTEGRATION +# elif (BOOST_VERSION < 107200) // 1.72 +# define CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION +# endif -#define CTI_DETAIL_ASIO_NAMESPACE_BEGIN \ - namespace boost { \ - namespace asio { -#define CTI_DETAIL_ASIO_NAMESPACE_END \ - } \ - } +# define CTI_DETAIL_ASIO_NAMESPACE_BEGIN \ + namespace boost { \ + namespace asio { +# define CTI_DETAIL_ASIO_NAMESPACE_END \ + } \ + } #endif #if defined(CTI_DETAIL_ASIO_HAS_NO_INTEGRATION) -#error "First-class ASIO support for continuable requires the form of "\ +# error "First-class ASIO support for continuable requires the form of "\ "`async_result` with an `initiate` static member function, which was added " \ "in standalone ASIO 1.13.0 and Boost ASIO 1.70. Older versions can be " \ "integrated manually with `cti::promisify`." #endif #if defined(CONTINUABLE_HAS_EXCEPTIONS) -#include +# include #endif namespace cti { @@ -96,40 +97,42 @@ namespace asio { using error_code_t = ::asio::error_code; using basic_errors_t = ::asio::error::basic_errors; -#if defined(CONTINUABLE_HAS_EXCEPTIONS) +# if defined(CONTINUABLE_HAS_EXCEPTIONS) using system_error_t = ::asio::system_error; -#endif +# endif #else using error_code_t = ::boost::system::error_code; using basic_errors_t = ::boost::asio::error::basic_errors; -#if defined(CONTINUABLE_HAS_EXCEPTIONS) +# if defined(CONTINUABLE_HAS_EXCEPTIONS) using system_error_t = ::boost::system::system_error; -#endif +# endif #endif // Binds `promise` to the first argument of a continuable resolver, giving it // the signature of an ASIO handler. -template -auto promise_resolver_handler(Promise&& promise) noexcept { - return [promise = std::forward(promise)]( - error_code_t e, auto&&... args) mutable noexcept { +template +auto promise_resolver_handler(Promise&& promise, Token&& token) noexcept { + return [promise = std::forward(promise), + token = std::forward(token)](error_code_t e, + auto&&... args) mutable noexcept { if (e) { - if (e != basic_errors_t::operation_aborted) { + if (!token.is_ignored(e)) { + if (token.is_cancellation(e)) { + promise.set_canceled(); + return; + } else { #if defined(CONTINUABLE_HAS_EXCEPTIONS) - promise.set_exception( - std::make_exception_ptr(system_error_t(std::move(e)))); + promise.set_exception( + std::make_exception_ptr(system_error_t(std::move(e)))); #else - promise.set_exception(exception_t(e.value(), e.category())); + promise.set_exception(exception_t(e.value(), e.category())); #endif - } else { - // Continuable uses a default constructed exception type to signal - // cancellation to the followed asynchronous control flow. - promise.set_exception(exception_t{}); + return; + } } - } else { - promise.set_value(std::forward(args)...); } + promise.set_value(std::forward(args)...); }; } @@ -153,8 +156,53 @@ struct initiate_make_continuable { template struct initiate_make_continuable - : initiate_make_continuable {}; + : initiate_make_continuable {}; +struct map_default { + constexpr map_default() noexcept {} + + bool is_cancellation(error_code_t const& ec) const noexcept { + // Continuable uses a default constructed exception type to signal + // cancellation to the followed asynchronous control flow. + return ec == basic_errors_t::operation_aborted; + } + bool is_ignored(error_code_t const& /*ec*/) const noexcept { + return false; + } +}; + +struct map_none { + constexpr map_none() noexcept {} + + bool is_cancellation(error_code_t const& /*ec*/) const noexcept { + return false; + } + bool is_ignored(error_code_t const& /*ec*/) const noexcept { + return false; + } +}; + +template +class map_ignore { +public: + map_ignore(std::array ignored) noexcept + : ignored_(ignored) {} + + bool is_cancellation(error_code_t const& ec) const noexcept { + return ec == basic_errors_t::operation_aborted; + } + bool is_ignored(error_code_t const& ec) const noexcept { + for (basic_errors_t ignored : ignored_) { + if (ec == ignored) { + return true; + } + } + return false; + } + +private: + std::array ignored_; +}; } // namespace asio } // namespace detail } // namespace cti diff --git a/include/continuable/external/asio.hpp b/include/continuable/external/asio.hpp index d4f4f8c..5ba2dff 100644 --- a/include/continuable/external/asio.hpp +++ b/include/continuable/external/asio.hpp @@ -35,6 +35,21 @@ #include namespace cti { +/// The error code type used by your asio distribution +/// +/// \since 4.1.0 +using asio_error_code_t = detail::asio::error_code_t; + +/// The basic error code enum used by your asio distribution +/// +/// \since 4.1.0 +using asio_basic_errors_t = detail::asio::basic_errors_t; + +/// The system error type used by your asio distribution +/// +/// \since 4.1.0 +using asio_system_error_t = detail::asio::system_error_t; + /// Type used as an ASIO completion token to specify an asynchronous operation /// that should return a continuable_base. /// @@ -60,19 +75,77 @@ namespace cti { /// }); /// ``` /// +/// \tparam Mapper The token can be instantiated with a custom mapper +/// for asio error codes which makes it possible to ignore +/// errors or treat them as cancellation types. +/// The mapper has the following form: +/// ``` +/// struct my_mapper { +/// constexpr my_mapper() noexcept {} +/// +/// /// Returns true when the error_code_t is a type which represents +/// /// cancellation and +/// bool is_cancellation(error_code_t const& /*ec*/) const noexcept { +/// return false; +/// } +/// bool is_ignored(error_code_t const& /*ec*/) const noexcept { +/// return false; +/// } +/// }; +/// ``` +/// +/// \attention `asio::error::basic_errors::operation_aborted` errors returned +/// by asio are automatically transformed into a default constructed +/// exception type which represents "operation canceled" by the +/// user or program. If you intend to retrieve the full +/// asio::error_code without remapping use the use_continuable_raw_t +/// completion token instead! +/// /// \since 4.0.0 -struct use_continuable_t {}; +template +struct use_continuable_t : public Mapper { + using Mapper::Mapper; +}; -/// Special value for instance of `asio_token_t` +/// \copydoc use_continuable_t +/// +/// The raw async completion handler token does not remap the asio error +/// `asio::error::basic_errors::operation_aborted` to a default constructed +/// exception type. +/// +/// \since 4.1.0 +using use_continuable_raw_t = use_continuable_t; + +/// Special value for instance of use_continuable_t which performs remapping +/// of asio error codes to align the cancellation behaviour with the library. /// /// \copydetails use_continuable_t -constexpr use_continuable_t use_continuable{}; +constexpr use_continuable_t<> use_continuable{}; + +/// Special value for instance of use_continuable_raw_t which doesn't perform +/// remapping of asio error codes and rethrows the raw error code. +/// +/// \copydetails use_continuable_raw_t +constexpr use_continuable_raw_t use_continuable_raw{}; + +/// Represents a special asio completion token which treats the given +/// asio basic error codes as success instead of failure. +/// +/// `asio::error::basic_errors::operation_aborted` is mapped +/// as cancellation token. +/// +/// \since 4.1.0 +template +auto use_continuable_ignoring(Args&&... args) noexcept { + return use_continuable_t>{ + {asio_basic_errors_t(std::forward(args))...}}; +} } // namespace cti CTI_DETAIL_ASIO_NAMESPACE_BEGIN -template -class async_result { +template +class async_result, Signature> { public: #if defined(CTI_DETAIL_ASIO_HAS_EXPLICIT_RET_TYPE_INTEGRATION) using return_type = typename cti::detail::asio::initiate_make_continuable< @@ -80,16 +153,16 @@ public: #endif template - static auto initiate(Initiation initiation, cti::use_continuable_t, - Args... args) { + static auto initiate(Initiation initiation, + cti::use_continuable_t token, Args... args) { return cti::detail::asio::initiate_make_continuable{}( - [initiation = std::move(initiation), + [initiation = std::move(initiation), token = std::move(token), init_args = std::make_tuple(std::move(args)...)]( auto&& promise) mutable { cti::detail::traits::unpack( [initiation = std::move(initiation), handler = cti::detail::asio::promise_resolver_handler( - std::forward(promise))]( + std::forward(promise), std::move(token))]( auto&&... args) mutable { std::move(initiation)(std::move(handler), std::forward(args)...); diff --git a/test/unit-test/async/test-continuable-async.cpp b/test/unit-test/async/test-continuable-async.cpp index 227170b..54def39 100644 --- a/test/unit-test/async/test-continuable-async.cpp +++ b/test/unit-test/async/test-continuable-async.cpp @@ -44,9 +44,7 @@ public: }) {} ~async_test_helper() { - assert(work_); - timer_.cancel(); - work_.reset(); + cancel(); thread_.join(); } @@ -55,6 +53,11 @@ public: return timer_.async_wait(use_continuable); } + void cancel() { + timer_.cancel(); + work_.reset(); + } + private: asio::io_context context_; asio::steady_timer timer_; @@ -141,3 +144,53 @@ TYPED_TEST(single_dimension_tests, wait_for_test_async) { result<> res = helper.wait_for(500ms).apply(cti::transforms::wait_for(50ms)); ASSERT_FALSE(res.is_exception()); } + +TYPED_TEST(single_dimension_tests, token_remap_canceled) { + asio::io_context io(1); + asio::steady_timer timer(io, 50ms); + + result<> value; + timer.async_wait(use_continuable).next([&](auto&&... args) { + value = result<>::from(std::forward(args)...); + }); + + timer.cancel(); + io.run(); + + ASSERT_TRUE(value.is_exception()); + ASSERT_FALSE(bool(value.get_exception())); +} + +TYPED_TEST(single_dimension_tests, token_remap_none_raw) { + asio::io_context io(1); + asio::steady_timer timer(io, 50ms); + + result<> value; + timer.async_wait(use_continuable_raw).next([&](auto&&... args) { + value = result<>::from(std::forward(args)...); + }); + + timer.cancel(); + io.run(); + + ASSERT_TRUE(value.is_exception()); + ASSERT_TRUE(bool(value.get_exception())); +} + +TYPED_TEST(single_dimension_tests, token_remap_ignore) { + asio::io_context io(1); + asio::steady_timer timer(io, 50ms); + + result<> value; + timer + .async_wait( + use_continuable_ignoring(asio_basic_errors_t::operation_aborted)) + .next([&](auto&&... args) { + value = result<>::from(std::forward(args)...); + }); + + timer.cancel(); + io.run(); + + ASSERT_TRUE(value.is_value()); +}