diff --git a/CHANGELOG.md b/CHANGELOG.md index 114b1f4b..a3037c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Version 229: * Rename to buffer_bytes * Tidy up examples * detect_ssl returns a bool +* Fix stable_async_base example API Changes: diff --git a/doc/qbk/quickref.xml b/doc/qbk/quickref.xml index e79f1cd5..07b1bb0f 100644 --- a/doc/qbk/quickref.xml +++ b/doc/qbk/quickref.xml @@ -31,7 +31,6 @@ file_stdio file_win32 flat_stream  - handler_ptr iequal iless rate_policy_access  diff --git a/include/boost/beast/core/async_base.hpp b/include/boost/beast/core/async_base.hpp index b701727e..ac066df9 100644 --- a/include/boost/beast/core/async_base.hpp +++ b/include/boost/beast/core/async_base.hpp @@ -468,12 +468,19 @@ public: using handler_type = typename net::async_completion::completion_handler_type; using base_type = stable_async_base; - struct op : base_type + struct op : base_type, boost::asio::coroutine { // This object must have a stable address struct temporary_data { + // Although std::string is in theory movable, most implementations + // use a "small buffer optimization" which means that we might + // be submitting a buffer to the write operation and then + // moving the string, invalidating the buffer. To prevent + // undefined behavior we store the string object itself at + // a stable location. std::string const message; + net::steady_timer timer; temporary_data(std::string message_, net::io_context& ctx) @@ -486,54 +493,55 @@ public: AsyncWriteStream& stream_; std::size_t repeats_; temporary_data& data_; - enum { starting, waiting, writing } state_; op(AsyncWriteStream& stream, std::size_t repeats, std::string message, handler_type& handler) : base_type(std::move(handler), stream.get_executor()) , stream_(stream) , repeats_(repeats) - , state_(starting) , data_(allocate_stable(*this, std::move(message), stream.get_executor().context())) { (*this)(); // start the operation } + // Including this file provides the keywords for macro-based coroutines + #include + void operator()(error_code ec = {}, std::size_t = 0) { - if (!ec) + reenter(*this) { - switch (state_) + // If repeats starts at 0 then we must complete immediately. But + // we can't call the final handler from inside the initiating + // function, so we post our intermediate handler first. We use + // net::async_write with an empty buffer instead of calling + // net::post to avoid an extra function template instantiation, to + // keep compile times lower and make the resulting executable smaller. + yield net::async_write(stream_, net::const_buffer{}, std::move(*this)); + while(! ec && repeats_-- > 0) { - case starting: - // If repeats starts at 0 then we must complete immediately. But we can't call the final - // handler from inside the initiating function, so we post our intermediate handler first. - if(repeats_ == 0) - return net::post(std::move(*this)); + // Send the string. We construct a `const_buffer` here to guarantee + // that we do not create an additional function template instantation + // of net::async_write, since we already instantiated it above for + // net::const_buffer. - case writing: - if (repeats_ > 0) - { - --repeats_; - state_ = waiting; - data_.timer.expires_after(std::chrono::seconds(1)); + yield net::async_write(stream_, + net::const_buffer(net::buffer(data_.message)), std::move(*this)); + if(ec) + break; - // Composed operation not yet complete. - return data_.timer.async_wait(std::move(*this)); - } - - // Composed operation complete, continue below. - break; - - case waiting: - // Composed operation not yet complete. - state_ = writing; - return net::async_write(stream_, net::buffer(data_.message), std::move(*this)); + // Set the timer and wait + data_.timer.expires_after(std::chrono::seconds(1)); + yield data_.timer.async_wait(std::move(*this)); } } - // The base class destroys the temporary data automatically, before invoking the final completion handler + // The base class destroys the temporary data automatically, + // before invoking the final completion handler this->complete_now(ec); } + + // Including this file undefines the macros for the coroutines + #include }; net::async_completion completion(handler); diff --git a/test/beast/core/async_base.cpp b/test/beast/core/async_base.cpp index 9c26039a..551b3723 100644 --- a/test/beast/core/async_base.cpp +++ b/test/beast/core/async_base.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -643,12 +644,19 @@ public: using handler_type = typename net::async_completion::completion_handler_type; using base_type = stable_async_base; - struct op : base_type + struct op : base_type, boost::asio::coroutine { // This object must have a stable address struct temporary_data { + // Although std::string is in theory movable, most implementations + // use a "small buffer optimization" which means that we might + // be submitting a buffer to the write operation and then + // moving the string, invalidating the buffer. To prevent + // undefined behavior we store the string object itself at + // a stable location. std::string const message; + net::steady_timer timer; temporary_data(std::string message_, net::io_context& ctx) @@ -658,14 +666,12 @@ public: } }; - enum { starting, waiting, writing } state_; AsyncWriteStream& stream_; std::size_t repeats_; temporary_data& data_; op(AsyncWriteStream& stream, std::size_t repeats, std::string message, handler_type& handler) : base_type(std::move(handler), stream.get_executor()) - , state_(starting) , stream_(stream) , repeats_(repeats) , data_(allocate_stable(*this, std::move(message), stream.get_executor().context())) @@ -673,42 +679,45 @@ public: (*this)(); // start the operation } + // Including this file provides the keywords for macro-based coroutines + #include + void operator()(error_code ec = {}, std::size_t = 0) { - if (!ec) + reenter(*this) { - switch (state_) + // If repeats starts at 0 then we must complete immediately. But + // we can't call the final handler from inside the initiating + // function, so we post our intermediate handler first. We use + // net::async_write with an empty buffer instead of calling + // net::post to avoid an extra function template instantiation, to + // keep compile times lower and make the resulting executable smaller. + yield net::async_write(stream_, net::const_buffer{}, std::move(*this)); + while(! ec && repeats_-- > 0) { - case starting: - // If repeats starts at 0 then we must complete immediately. But we can't call the final - // handler from inside the initiating function, so we post our intermediate handler first. - if(repeats_ == 0) - return net::post(std::move(*this)); + // Send the string. We construct a `const_buffer` here to guarantee + // that we do not create an additional function template instantation + // of net::async_write, since we already instantiated it above for + // net::const_buffer. - case writing: - if (repeats_ > 0) - { - --repeats_; - state_ = waiting; - data_.timer.expires_after(std::chrono::seconds(1)); + yield net::async_write(stream_, + net::const_buffer(net::buffer(data_.message)), std::move(*this)); + if(ec) + break; - // Composed operation not yet complete. - return data_.timer.async_wait(std::move(*this)); - } - - // Composed operation complete, continue below. - break; - - case waiting: - // Composed operation not yet complete. - state_ = writing; - return net::async_write(stream_, net::buffer(data_.message), std::move(*this)); + // Set the timer and wait + data_.timer.expires_after(std::chrono::seconds(1)); + yield data_.timer.async_wait(std::move(*this)); } } - // The base class destroys the temporary data automatically, before invoking the final completion handler + // The base class destroys the temporary data automatically, + // before invoking the final completion handler this->complete_now(ec); } + + // Including this file undefines the macros for the coroutines + #include }; net::async_completion completion(handler);