diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index b961be36..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,64 +0,0 @@ -# CLAUDE.md - -## Code Style - -- C++11 unless otherwise specified -- Boost C++ Libraries naming conventions (snake_case) -- 4-space indentation, no tabs -- Braces on their own line for classes/functions - -## Javadoc Documentation - -Follow Boost C++ Libraries Javadoc style: - -- Brief descriptions on first line after `/**` -- Functions returning values: brief starts with "Return" -- Use `@param` for function parameters -- Use `@tparam` for template parameters, except: - - Variadic args (`Args...`) — omit - - Types deduced from function parameters — omit (self-evident from `@param`) -- Use `@return` for return value details -- Use `@pre` for preconditions -- Use `@post` for postconditions -- Use `@throws` for exceptions -- Use `@note` for important notes -- Use `@see` for cross-references -- Use `@code` / `@endcode` for examples - -## Examples - -```cpp -/** Return the size of the buffer sequence. - - @param buffers The buffer sequence to measure. - - @return The total byte count. -*/ -template -std::size_t -buffer_size(BufferSequence const& buffers); -``` - -No `@tparam` needed—`BufferSequence` is evident from `@param buffers`. - -```cpp -/** Return the default value. - - @tparam T The value type. -*/ -template -T default_value(); -``` - -`@tparam` needed—`T` has no corresponding function parameter. - -## Preferences - -- Concise, dry answers -- Full files, not diffs -- Accurate, compiling C++ code - -## Preconditions and Postconditions - -- The caller of any function returning a route_result must immediately return the route_result and cannot perform other actions -- route handlers which return a route_result must never return a system::error_code which would return false from failed() \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f581903..f73fe086 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,10 +57,20 @@ set(BOOST_SRC_DIR ${DEFAULT_BOOST_SRC_DIR} CACHE STRING "Boost source dir to use set(BOOST_BEAST2_DEPENDENCIES Boost::asio Boost::assert + Boost::buffers + Boost::capy Boost::config + Boost::core + Boost::describe + Boost::endian Boost::http_proto + Boost::json + Boost::mp11 + Boost::static_assert Boost::system Boost::throw_exception + Boost::url + Boost::variant2 ) foreach (BOOST_BEAST2_DEPENDENCY ${BOOST_BEAST2_DEPENDENCIES}) @@ -70,10 +80,10 @@ foreach (BOOST_BEAST2_DEPENDENCY ${BOOST_BEAST2_DEPENDENCIES}) endforeach () # Conditional dependencies if (BOOST_BEAST2_BUILD_TESTS) - set(BOOST_BEAST2_UNIT_TEST_LIBRARIES beast url) + set(BOOST_BEAST2_UNIT_TEST_LIBRARIES beast) endif () if (BOOST_BEAST2_BUILD_EXAMPLES) - set(BOOST_BEAST2_EXAMPLE_LIBRARIES json program_options scope url multiprecision) + set(BOOST_BEAST2_EXAMPLE_LIBRARIES program_options scope multiprecision) endif () # Complete dependency list set(BOOST_INCLUDE_LIBRARIES ${BOOST_BEAST2_INCLUDE_LIBRARIES} ${BOOST_BEAST2_UNIT_TEST_LIBRARIES} ${BOOST_BEAST2_EXAMPLE_LIBRARIES}) @@ -115,7 +125,7 @@ if (BOOST_BEAST2_IS_ROOT) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${BOOST_SRC_DIR}/tools/cmake/include") else () # From Boost Package - find_package(Boost REQUIRED COMPONENTS buffers http_proto program_options scope url) + find_package(Boost REQUIRED COMPONENTS buffers capy http_proto json program_options scope system url) foreach (BOOST_INCLUDE_LIBRARY ${BOOST_INCLUDE_LIBRARIES}) if (NOT TARGET Boost::${BOOST_INCLUDE_LIBRARY}) add_library(Boost::${BOOST_INCLUDE_LIBRARY} ALIAS Boost::headers) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 36fb1b41..f9abbaca 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -10,4 +10,3 @@ add_subdirectory(client) add_subdirectory(server) -add_subdirectory(cpp20) diff --git a/example/Jamfile b/example/Jamfile index b92d59e0..7d19c3d5 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -10,4 +10,3 @@ build-project client ; build-project server ; -build-project cpp20 ; diff --git a/example/cpp20/CMakeLists.txt b/example/cpp20/CMakeLists.txt deleted file mode 100644 index 21d46b2d..00000000 --- a/example/cpp20/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# -# Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -# -# Official repository: https://github.com/cppalliance/beast2 -# - -add_subdirectory(co_spawn) diff --git a/example/cpp20/Jamfile b/example/cpp20/Jamfile deleted file mode 100644 index 2e6efaaa..00000000 --- a/example/cpp20/Jamfile +++ /dev/null @@ -1,10 +0,0 @@ -# -# Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -# -# Official repository: https://github.com/cppalliance/beast2 -# - -build-project co_spawn ; diff --git a/example/cpp20/co_spawn/CMakeLists.txt b/example/cpp20/co_spawn/CMakeLists.txt deleted file mode 100644 index 115b26af..00000000 --- a/example/cpp20/co_spawn/CMakeLists.txt +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright (c) 2025 Mohammad Nejati -# -# 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) -# -# Official repository: https://github.com/cppalliance/beast2 -# - -if(NOT cxx_std_20 IN_LIST CMAKE_CXX_COMPILE_FEATURES) - return() -endif() - -file(GLOB_RECURSE PFILES CONFIGURE_DEPENDS *.cpp *.hpp - CMakeLists.txt - Jamfile) - -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${PFILES}) - -add_executable(beast2_example_co_spawn ${PFILES}) - -target_compile_definitions(beast2_example_co_spawn - PRIVATE BOOST_ASIO_NO_DEPRECATED) - -set_property(TARGET beast2_example_co_spawn - PROPERTY FOLDER "examples") - -find_package(OpenSSL REQUIRED) - -target_compile_features(beast2_example_co_spawn PUBLIC cxx_std_20) - -target_link_libraries(beast2_example_co_spawn - Boost::beast2) diff --git a/example/cpp20/co_spawn/Jamfile b/example/cpp20/co_spawn/Jamfile deleted file mode 100644 index 77befb7b..00000000 --- a/example/cpp20/co_spawn/Jamfile +++ /dev/null @@ -1,41 +0,0 @@ -# -# Copyright (c) 2025 Mohammad Nejati -# -# 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) -# -# Official repository: https://github.com/cppalliance/beast2 -# - -import config : requires ; - -using openssl ; -import ac ; - -lib advapi32 ; -lib crypt32 ; -lib gdi32 ; -lib user32 ; - -project - : requirements - /boost/beast2//boost_beast2 - [ ac.check-library /boost/capy//boost_capy_zlib : /boost/capy//boost_capy_zlib : ] - [ ac.check-library /boost/capy//boost_capy_brotli : /boost/capy//boost_capy_brotli : ] - [ ac.check-library /openssl//ssl : /openssl//ssl/shared : no ] - [ ac.check-library /openssl//crypto : /openssl//crypto/shared : no ] - windows:advapi32 - windows:crypt32 - windows:gdi32 - windows:user32 - /boost/url//boost_url - . - ; - -exe get : - [ glob *.cpp ] - : requirements - [ requires - cxx20_hdr_coroutine - ] - ; diff --git a/example/cpp20/co_spawn/async_42.hpp b/example/cpp20/co_spawn/async_42.hpp deleted file mode 100644 index c9d2e198..00000000 --- a/example/cpp20/co_spawn/async_42.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/capy -// - -#ifndef BOOST_BEAST2_TASK_HPP -#define BOOST_BEAST2_TASK_HPP - -#include -#include - -namespace boost { -namespace beast2 { - -template -auto async_42(Executor const& exec, CompletionToken&& token) -{ - return asio::async_initiate( - [](auto handler, Executor exec) - { - boost::asio::post(exec, - [handler = std::move(handler)]() mutable - { - std::move(handler)(42); - }); - }, - token, - exec); -} - -} // beast2 -} // boost - -#endif diff --git a/example/cpp20/co_spawn/async_result.hpp b/example/cpp20/co_spawn/async_result.hpp deleted file mode 100644 index d40ec7f5..00000000 --- a/example/cpp20/co_spawn/async_result.hpp +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/capy -// - -#ifndef BOOST_CAPY_ASYNC_RESULT_HPP -#define BOOST_CAPY_ASYNC_RESULT_HPP - -#include -#include -#include -#include -#include - -namespace boost { -namespace capy { - -template -class async_result -{ -public: - struct impl_base - { - virtual ~impl_base() = default; - virtual void start(std::function on_done) = 0; - virtual T get_result() = 0; - }; - -private: - std::unique_ptr impl_; - -public: - explicit async_result(std::unique_ptr p) : impl_(std::move(p)) {} - - async_result(async_result&&) = default; - async_result& operator=(async_result&&) = default; - - bool await_ready() const noexcept { return false; } - - void await_suspend(std::coroutine_handle<> h) - { - impl_->start([h]{ h.resume(); }); - } - - T await_resume() - { - return impl_->get_result(); - } -}; - -//----------------------------------------------------------------------------- - -template -struct async_result_impl : capy::async_result::impl_base -{ - DeferredOp op_; - std::variant result_; - - explicit async_result_impl(DeferredOp&& op) - : op_(std::forward(op)) - { - } - - void start(std::function on_done) override - { - std::move(op_)( - [this, on_done = std::move(on_done)](auto&&... args) mutable - { - result_.template emplace<1>(T{std::forward(args)...}); - on_done(); - }); - } - - T get_result() override - { - if (result_.index() == 0 && std::get<0>(result_)) - std::rethrow_exception(std::get<0>(result_)); - return std::move(std::get<1>(result_)); - } -}; - -//----------------------------------------------------------------------------- - -template -capy::async_result -make_async_result(DeferredOp&& op) -{ - using impl_type = async_result_impl>; - return capy::async_result( - std::make_unique(std::forward(op))); -} - -} // capy -} // boost - -#endif diff --git a/example/cpp20/co_spawn/main.cpp b/example/cpp20/co_spawn/main.cpp deleted file mode 100644 index f61d8ac6..00000000 --- a/example/cpp20/co_spawn/main.cpp +++ /dev/null @@ -1,93 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/beast2 -// - -#include "async_42.hpp" -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace boost { - -namespace buffers { - -auto read_all( - any_stream s, - buffers::mutable_buffer b) - -> capy::task -{ - std::size_t total = 0; - while(b.size() > 0) - { - auto [ec, n] = co_await s.read_some(b); - total += n; - if(ec.failed()) - co_return { ec, total }; - buffers::remove_prefix(b, n); - } - co_return { {}, total }; -} - -} // buffers - -namespace beast2 { - -struct stream -{ - asio::any_io_executor ex_; - - template - stream(Executor const& ex) - : ex_(ex) - { - } - - capy::async_result - read_some() - { - return capy::make_async_result( - async_42(ex_, asio::deferred)); - } -}; - -} // beast2 - -capy::task handler() -{ - co_return 42; -} - -void boost_main() -{ - asio::io_context ioc; - - beast2::spawn( - ioc.get_executor(), - handler(), - [](std::variant result) - { - if (result.index() == 0) - std::rethrow_exception(std::get<0>(result)); - std::cout << "result: " << std::get<1>(result) << "\n"; - }); - - ioc.run(); -} - -} // boost - -int main(int, char**) -{ - boost::boost_main(); -} diff --git a/example/cpp20/co_spawn/stream.hpp b/example/cpp20/co_spawn/stream.hpp deleted file mode 100644 index 73ef10ef..00000000 --- a/example/cpp20/co_spawn/stream.hpp +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/capy -// - -#ifndef BOOST_CAPY_STREAM_HPP -#define BOOST_CAPY_STREAM_HPP - -#include "task.hpp" - -namespace boost { -namespace capy { - -class stream -{ - -}; - -} // capy -} // boost - -#endif diff --git a/example/server/main.cpp b/example/server/main.cpp index 47cbc73b..10bc19bd 100644 --- a/example/server/main.cpp +++ b/example/server/main.cpp @@ -8,7 +8,6 @@ // #include "certificate.hpp" -#include "post_work.hpp" #include "serve_detached.hpp" #include "serve_log_admin.hpp" #include @@ -17,9 +16,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -30,6 +31,8 @@ namespace boost { namespace beast2 { +capy::thread_pool g_tp; + void install_services(capy::application& app) { #ifdef BOOST_CAPY_HAS_BROTLI @@ -102,7 +105,7 @@ struct do_json_rpc return http::route::next; return rp.read_body( json_sink(), - [this, &rp]( + [this]( json::value jv) -> http::route_result { @@ -121,6 +124,46 @@ struct do_json_rpc }; +#ifdef BOOST_BEAST2_HAS_CORO +auto +my_coro( + http::route_params& rp) -> + capy::task +{ + (void)rp; + asio::thread_pool tp(1); + co_await capy::make_async_op( + [&tp](auto&& handler) + { + asio::post(tp.get_executor(), + [handler = std::move(handler)]() mutable + { + // Simulate some asynchronous work + std::this_thread::sleep_for(std::chrono::seconds(1)); + handler(); + }); + }); + co_return http::route::next; +} + +auto +do_bcrypt( + http::route_params& rp) -> + capy::task +{ + std::string password = "boost"; + //auto rv = co_await capy::bcrypt::async_hash(password); + co_await g_tp.get_executor().submit( + []() + { + std::this_thread::sleep_for(std::chrono::seconds(1)); + }); + co_return http::route::next; +} + + +#endif + int server_main( int argc, char* argv[] ) { try @@ -150,6 +193,14 @@ int server_main( int argc, char* argv[] ) http::cors(opts), do_json_rpc() ); +#ifdef BOOST_BEAST2_HAS_CORO + srv.wwwroot.use( + "/spawn", + http::co_route(my_coro)); + srv.wwwroot.use( + "/bcrypt", + http::co_route(do_bcrypt)); +#endif srv.wwwroot.use("/", serve_static( argv[3] )); app.start(); diff --git a/example/server/post_work.cpp b/example/server/post_work.cpp deleted file mode 100644 index eca5a121..00000000 --- a/example/server/post_work.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright (c) 2022 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/beast2 -// - -#include "post_work.hpp" - -namespace boost { -namespace beast2 { - -namespace { - -struct task -{ - std::size_t i = 10; - - void - operator()(http_proto::resumer resume) - { - if(i--) - return; - resume(http_proto::route::next); - } -}; - -} // (anon) - -//------------------------------------------------ - -http_proto::route_result -post_work:: -operator()( - http_proto::route_params& p) const -{ - return p.post(task()); -} - -} // beast2 -} // boost diff --git a/example/server/post_work.hpp b/example/server/post_work.hpp deleted file mode 100644 index b75c398d..00000000 --- a/example/server/post_work.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/beast2 -// - -#ifndef BOOST_BEAST2_SERVER_POST_WORK_HPP -#define BOOST_BEAST2_SERVER_POST_WORK_HPP - -#include -#include - -namespace boost { -namespace beast2 { - -struct post_work -{ - system::error_code - operator()( - http_proto::route_params&) const; -}; - -} // beast2 -} // boost - -#endif diff --git a/include/boost/beast2.hpp b/include/boost/beast2.hpp index 0b17a646..d7de8709 100644 --- a/include/boost/beast2.hpp +++ b/include/boost/beast2.hpp @@ -34,12 +34,12 @@ #include #include #include +#include #include #include #include #include #include -#include //#include #include diff --git a/include/boost/beast2/detail/config.hpp b/include/boost/beast2/detail/config.hpp index f80fc1d7..9e00d20d 100644 --- a/include/boost/beast2/detail/config.hpp +++ b/include/boost/beast2/detail/config.hpp @@ -39,9 +39,9 @@ namespace beast2 { //------------------------------------------------ #if defined(__cpp_lib_coroutine) && __cpp_lib_coroutine >= 201902L -# define BOOST_BEAST_HAS_CORO 1 +# define BOOST_BEAST2_HAS_CORO 1 #elif defined(__cpp_impl_coroutine) && __cpp_impl_coroutines >= 201902L -# define BOOST_BEAST_HAS_CORO 1 +# define BOOST_BEAST2_HAS_CORO 1 #endif //------------------------------------------------ diff --git a/include/boost/beast2/server/http_stream.hpp b/include/boost/beast2/server/http_stream.hpp index 4c3ae495..265ff469 100644 --- a/include/boost/beast2/server/http_stream.hpp +++ b/include/boost/beast2/server/http_stream.hpp @@ -12,13 +12,14 @@ #include #include +#include #include #include -#include #include #include #include -#include +#include +#include #include #include #include @@ -101,7 +102,8 @@ class http_stream std::size_t bytes_transferred); void on_complete(); http::resumer do_suspend() override; - void do_resume(http::route_result const& ec) override; + void do_resume(http::route_result const& rv) override; + void do_resume(std::exception_ptr ep) override; void do_close(); void do_fail(core::string_view s, system::error_code const& ec); @@ -182,6 +184,7 @@ http_stream( rp_.serializer = http::serializer(app); rp_.suspend = http::suspender(*this); + rp_.ex = wrap_executor(stream_.get_executor()); } // called to start a new HTTP session. @@ -444,6 +447,40 @@ do_resume( }); } +// called by resume(ep) +template +void +http_stream:: +do_resume( + std::exception_ptr ep) +{ + asio::dispatch( + stream_.get_executor(), + [this, ep] + { + BOOST_ASSERT(pwg_.get() != nullptr); + pwg_.reset(); + + rp_.status(http::status::internal_server_error); + try + { + std::rethrow_exception(ep); + } + catch(std::exception const& e) + { + std::string s; + format_to(s, "An internal server error occurred: {}", e.what()); + rp_.set_body(s); + } + catch(...) + { + rp_.set_body("An internal server error occurred"); + } + rp_.res.set_keep_alive(false); + do_write(); + }); +} + // called when a non-recoverable error occurs template void diff --git a/include/boost/beast2/server/plain_worker.hpp b/include/boost/beast2/server/plain_worker.hpp index 2b630960..8a7c40f7 100644 --- a/include/boost/beast2/server/plain_worker.hpp +++ b/include/boost/beast2/server/plain_worker.hpp @@ -20,16 +20,21 @@ #include #include #include +#include namespace boost { namespace beast2 { template class plain_worker - : public http_stream< - asio::basic_stream_socket - > + : private boost::base_from_member< + asio::basic_stream_socket> + , public http_stream< + asio::basic_stream_socket> { + using base_member = boost::base_from_member< + asio::basic_stream_socket>; + public: using executor_type = Executor; using protocol_type = Protocol; @@ -51,7 +56,7 @@ class plain_worker socket_type& socket() noexcept { - return stream_; + return this->member; } typename Protocol::endpoint& @@ -78,7 +83,6 @@ class plain_worker void do_close(system::error_code const& ec); workers_base& wb_; - stream_type stream_; typename Protocol::endpoint ep_; }; @@ -91,16 +95,16 @@ plain_worker( workers_base& wb, Executor0 const& ex, router_asio rr) - : http_stream( + : base_member(Executor(ex)) + , http_stream( wb.app(), - stream_, + this->member, std::move(rr), [this](system::error_code const& ec) { this->do_close(ec); }) , wb_(wb) - , stream_(Executor(ex)) { } @@ -110,7 +114,7 @@ plain_worker:: cancel() { system::error_code ec; - stream_.cancel(ec); + this->member.cancel(ec); } //-------------------------------------------- @@ -121,7 +125,7 @@ void plain_worker:: on_accept(acceptor_config const* pconfig) { - BOOST_ASSERT(stream_.get_executor().running_in_this_thread()); + BOOST_ASSERT(this->member.get_executor().running_in_this_thread()); // VFALCO TODO timeout this->on_stream_begin(*pconfig); } @@ -156,7 +160,7 @@ reset() { // Clean up any previous connection. system::error_code ec; - stream_.close(ec); + this->member.close(ec); } /** Close the connection to end the session diff --git a/include/boost/beast2/server/route_handler_asio.hpp b/include/boost/beast2/server/route_handler_asio.hpp index 9dfde614..d93cf4d2 100644 --- a/include/boost/beast2/server/route_handler_asio.hpp +++ b/include/boost/beast2/server/route_handler_asio.hpp @@ -11,9 +11,7 @@ #define BOOST_BEAST2_SERVER_ROUTE_HANDLER_ASIO_HPP #include -#include #include -#include #include namespace boost { @@ -51,55 +49,8 @@ class asio_route_params } } -private: - -#ifdef BOOST_CAPY_HAS_CORO - auto - spawn( - capy::task t) -> - http::route_result override - { - return this->suspend( - [&](http::resumer resume) - { - beast2::spawn( - stream.get_executor(), - std::move(t), - [resume](std::variant< - std::exception_ptr, - http::route_result> v) - { - if(v.index() == 0) - { - std::rethrow_exception( - std::get<0>(v)); - } - resume(std::get<1>(v)); - }); - }); - } -#endif - - void do_post() override; }; -//----------------------------------------------- - -template -void -asio_route_params:: -do_post() -{ - asio::post( - stream.get_executor(), - [&] - { - if(task_->invoke()) - return; - do_post(); - }); -} - } // beast2 } // boost diff --git a/include/boost/beast2/server/worker_ssl.hpp b/include/boost/beast2/server/worker_ssl.hpp index 0d0f38af..52125fa6 100644 --- a/include/boost/beast2/server/worker_ssl.hpp +++ b/include/boost/beast2/server/worker_ssl.hpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace boost { @@ -38,10 +39,15 @@ template< class Executor, class Protocol = asio::ip::tcp > -class worker_ssl : public http_stream< - ssl_stream> -> +class worker_ssl + : private boost::base_from_member< + ssl_stream>> + , public http_stream< + ssl_stream>> { + using base_member = boost::base_from_member< + ssl_stream>>; + public: using executor_type = Executor; using protocol_type = Protocol; @@ -68,7 +74,7 @@ class worker_ssl : public http_stream< socket_type& socket() noexcept { - return stream_.next_layer(); + return this->member.next_layer(); } typename Protocol::endpoint& @@ -102,7 +108,6 @@ class worker_ssl : public http_stream< workers_base& wb_; asio::ssl::context& ssl_ctx_; - stream_type stream_; typename Protocol::endpoint ep_; }; @@ -116,9 +121,10 @@ worker_ssl( Executor0 const& ex, asio::ssl::context& ssl_ctx, router_asio rr) - : http_stream( + : base_member(Executor(ex), ssl_ctx) + , http_stream( wb.app(), - stream_, + this->member, std::move(rr), [this](system::error_code const& ec) { @@ -126,7 +132,6 @@ worker_ssl( }) , wb_(wb) , ssl_ctx_(ssl_ctx) - , stream_(Executor(ex), ssl_ctx) { } @@ -136,7 +141,7 @@ worker_ssl:: cancel() { system::error_code ec; - stream_.next_layer().cancel(ec); + this->member.next_layer().cancel(ec); } //-------------------------------------------- @@ -147,12 +152,12 @@ void worker_ssl:: on_accept(acceptor_config const* pconfig) { - BOOST_ASSERT(stream_.get_executor().running_in_this_thread()); + BOOST_ASSERT(this->member.get_executor().running_in_this_thread()); // VFALCO TODO timeout - stream_.set_ssl(pconfig->is_ssl); + this->member.set_ssl(pconfig->is_ssl); if(! pconfig->is_ssl) return this->on_accept(*pconfig); - return stream_.stream().async_handshake( + return this->member.stream().async_handshake( asio::ssl::stream_base::server, asio::prepend(call_mf( &worker_ssl::on_handshake, this), pconfig)); @@ -187,7 +192,7 @@ on_shutdown(system::error_code ec) "{} worker_ssl::on_shutdown", this->id()); - stream_.next_layer().shutdown( + this->member.next_layer().shutdown( asio::socket_base::shutdown_both, ec); // error ignored @@ -225,14 +230,14 @@ reset() { // Clean up any previous connection. system::error_code ec; - stream_.next_layer().close(ec); + this->member.next_layer().close(ec); // asio::ssl::stream has an internal state which cannot be reset. // In order to perform the handshake again, we destroy the old // object and assign a new one, in a way that preserves the // original socket to avoid churning file handles. // - stream_ = stream_type(std::move(stream_.next_layer()), ssl_ctx_); + this->member = stream_type(std::move(this->member.next_layer()), ssl_ctx_); } /** Close the connection to end the session @@ -250,7 +255,7 @@ do_close(system::error_code const& ec) wb_.do_idle(this); return; } - stream_.stream().async_shutdown(call_mf( + this->member.stream().async_shutdown(call_mf( &worker_ssl::on_shutdown, this)); return; } diff --git a/include/boost/beast2/spawn.hpp b/include/boost/beast2/spawn.hpp deleted file mode 100644 index 4912f04e..00000000 --- a/include/boost/beast2/spawn.hpp +++ /dev/null @@ -1,85 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) -// -// Official repository: https://github.com/cppalliance/beast2 -// - -#ifndef BOOST_BEAST2_SPAWN_HPP -#define BOOST_BEAST2_SPAWN_HPP - -#include - -#ifdef BOOST_CAPY_HAS_CORO - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace boost { -namespace beast2 { - -/** Launch a capy::task on the given executor. - - This function is similar to boost::asio::co_spawn, but is used to - launch a capy::task instead of an asio::awaitable. - @param ex The executor on which the task will be launched. - @param t The task to launch. - @param handler The completion handler to be invoked when the task - completes. The handler signature is: - @code - void(std::variant) - @endcode - where the variant holds either an exception_ptr if an exception - was thrown, or the result of type T. - @return The result of the asynchronous initiation. -*/ -template< - class Executor, - class T, - class CompletionHandler> -auto spawn( - Executor const& ex, - capy::task t, - CompletionHandler&& handler) -{ - return asio::async_initiate< - CompletionHandler, - void(std::variant)>( - [ex_ = ex](auto handler, capy::task t) - { - auto h = t.release(); - auto* p = &h.promise(); - - p->on_done_ = [handler = std::move(handler), h, p]() mutable - { - auto& r = p->result_; - if (r.index() == 2) - std::move(handler)(std::variant( - std::in_place_index<0>, std::get<2>(r))); - else - std::move(handler)(std::variant( - std::in_place_index<1>, std::move(std::get<1>(r)))); - h.destroy(); - }; - - asio::post(ex_, [h]{ h.resume(); }); - }, - handler, - std::move(t)); -} - -} // beast2 -} // boost - -#endif - -#endif diff --git a/include/boost/beast2/wrap_executor.hpp b/include/boost/beast2/wrap_executor.hpp new file mode 100644 index 00000000..ec98633d --- /dev/null +++ b/include/boost/beast2/wrap_executor.hpp @@ -0,0 +1,181 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) +// +// Official repository: https://github.com/cppalliance/beast2 +// + +#ifndef BOOST_BEAST2_WRAP_EXECUTOR_HPP +#define BOOST_BEAST2_WRAP_EXECUTOR_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace beast2 { + +namespace detail { + +// Metadata stored before each work allocation +struct work_metadata +{ + void* raw; + std::size_t total_size; +}; + +template +struct asio_executor_impl +{ + friend struct capy::executor::access; + + using allocator_type = typename + asio::associated_allocator::type; + + AsioExecutor exec_; + allocator_type alloc_; + + explicit + asio_executor_impl(AsioExecutor ex) + : exec_(std::move(ex)) + , alloc_(asio::get_associated_allocator(exec_)) + { + } + + // Move constructor + asio_executor_impl(asio_executor_impl&& other) noexcept + : exec_(std::move(other.exec_)) + , alloc_(std::move(other.alloc_)) + { + } + + // Move assignment + asio_executor_impl& operator=(asio_executor_impl&& other) noexcept + { + if(this != &other) + { + exec_ = std::move(other.exec_); + alloc_ = std::move(other.alloc_); + } + return *this; + } + + // Delete copy operations + asio_executor_impl(asio_executor_impl const&) = delete; + asio_executor_impl& operator=(asio_executor_impl const&) = delete; + +private: + void* + allocate(std::size_t size, std::size_t align) + { + // Rebind allocator to char for byte-level allocation + using char_alloc = typename std::allocator_traits< + allocator_type>::template rebind_alloc; + char_alloc a(alloc_); + + // We need space for: + // - metadata struct (aligned to work_metadata alignment) + // - padding for work alignment + // - work object + std::size_t const meta_size = sizeof(work_metadata); + std::size_t const total_size = meta_size + align + size; + + // Allocate raw storage + char* raw = std::allocator_traits::allocate(a, total_size); + + // Compute aligned pointer for work after metadata + char* after_meta = raw + meta_size; + void* aligned = after_meta; + std::size_t space = total_size - meta_size; + aligned = std::align(align, size, aligned, space); + + // Store metadata immediately before the aligned work region + work_metadata* meta = reinterpret_cast( + static_cast(aligned) - sizeof(work_metadata)); + meta->raw = raw; + meta->total_size = total_size; + + return aligned; + } + + void + deallocate(void* p, std::size_t /*size*/, std::size_t /*align*/) + { + using char_alloc = typename std::allocator_traits< + allocator_type>::template rebind_alloc; + char_alloc a(alloc_); + + // Retrieve metadata stored before p + work_metadata* meta = reinterpret_cast( + static_cast(p) - sizeof(work_metadata)); + + std::allocator_traits::deallocate( + a, static_cast(meta->raw), meta->total_size); + } + + void + submit(capy::executor::work* w) + { + // Capture a copy of allocator for the lambda + allocator_type alloc_copy = alloc_; + asio::post(exec_, + [w, alloc_copy]() mutable + { + using char_alloc = typename std::allocator_traits< + allocator_type>::template rebind_alloc; + char_alloc a(alloc_copy); + + // Retrieve metadata stored before w + work_metadata* meta = reinterpret_cast( + reinterpret_cast(w) - sizeof(work_metadata)); + void* raw = meta->raw; + std::size_t total_size = meta->total_size; + + w->invoke(); + w->~work(); + + std::allocator_traits::deallocate( + a, static_cast(raw), total_size); + }); + } +}; + +} // detail + +/** Return a capy::executor from an Asio executor. + + This function wraps an Asio executor in a capy::executor, + mapping the capy::executor implementation API to the + corresponding Asio executor operations. + + The returned executor uses get_associated_allocator on + the Asio executor to obtain the allocator for work items. + + @param ex The Asio executor to wrap. + + @return A capy::executor that submits work via the + provided Asio executor. +*/ +template +capy::executor +wrap_executor(AsioExecutor ex) +{ + return capy::executor::wrap( + detail::asio_executor_impl< + typename std::decay::type>( + std::move(ex))); +} + +} // beast2 +} // boost + +#endif + diff --git a/test/unit/stream.cpp b/test/unit/stream.cpp index d0a51ed9..1e1fb2ae 100644 --- a/test/unit/stream.cpp +++ b/test/unit/stream.cpp @@ -11,7 +11,6 @@ //#include #include -#include #include #include #include diff --git a/test/unit/wrap_executor.cpp b/test/unit/wrap_executor.cpp new file mode 100644 index 00000000..cf643776 --- /dev/null +++ b/test/unit/wrap_executor.cpp @@ -0,0 +1,603 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail 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) +// +// Official repository: https://github.com/cppalliance/beast2 +// + +// Test that header file is self-contained. +#include + +#include "test_helpers.hpp" + +#include +#include +#include +#include + +namespace boost { +namespace beast2 { + +//----------------------------------------------------------------------------- + +/** Tracking allocator to verify allocator usage. +*/ +template +struct tracking_allocator +{ + using value_type = T; + + std::shared_ptr> alloc_count; + std::shared_ptr> dealloc_count; + + tracking_allocator() + : alloc_count(std::make_shared>(0)) + , dealloc_count(std::make_shared>(0)) + { + } + + template + tracking_allocator(tracking_allocator const& other) + : alloc_count(other.alloc_count) + , dealloc_count(other.dealloc_count) + { + } + + T* allocate(std::size_t n) + { + ++(*alloc_count); + return static_cast(::operator new(n * sizeof(T))); + } + + void deallocate(T* p, std::size_t) + { + ++(*dealloc_count); + ::operator delete(p); + } + + template + bool operator==(tracking_allocator const& other) const + { + return alloc_count == other.alloc_count; + } + + template + bool operator!=(tracking_allocator const& other) const + { + return !(*this == other); + } +}; + +/** Executor wrapper that associates an allocator. +*/ +template +struct executor_with_allocator +{ + Executor exec_; + Allocator alloc_; + + using inner_executor_type = Executor; + + executor_with_allocator( + Executor ex, + Allocator alloc) + : exec_(std::move(ex)) + , alloc_(std::move(alloc)) + { + } + + Executor const& get_inner_executor() const noexcept + { + return exec_; + } + + Allocator get_allocator() const noexcept + { + return alloc_; + } + + // Forward executor operations + auto context() const noexcept -> decltype(exec_.context()) + { + return exec_.context(); + } + + void on_work_started() const noexcept + { + exec_.on_work_started(); + } + + void on_work_finished() const noexcept + { + exec_.on_work_finished(); + } + + template + void dispatch(F&& f, A const& a) const + { + exec_.dispatch(std::forward(f), a); + } + + template + void post(F&& f, A const& a) const + { + exec_.post(std::forward(f), a); + } + + template + void defer(F&& f, A const& a) const + { + exec_.defer(std::forward(f), a); + } + + bool operator==(executor_with_allocator const& other) const noexcept + { + return exec_ == other.exec_; + } + + bool operator!=(executor_with_allocator const& other) const noexcept + { + return exec_ != other.exec_; + } +}; + +} // beast2 + +// Specialize associated_allocator for our wrapper +namespace asio { + +template +struct associated_allocator< + beast2::executor_with_allocator, + DefaultAllocator> +{ + using type = Allocator; + + static type get( + beast2::executor_with_allocator const& ex, + DefaultAllocator const& = DefaultAllocator()) noexcept + { + return ex.get_allocator(); + } +}; + +} // asio + +namespace beast2 { + +//----------------------------------------------------------------------------- + +struct wrap_executor_test +{ + void + testWrapExecutor() + { + asio::io_context ioc; + capy::executor ex = wrap_executor(ioc.get_executor()); + BOOST_TEST(static_cast(ex)); + } + + void + testPostLambda() + { + asio::io_context ioc; + capy::executor ex = wrap_executor(ioc.get_executor()); + + bool called = false; + ex.post([&called]{ called = true; }); + + BOOST_TEST(!called); + test::run(ioc); + BOOST_TEST(called); + } + + void + testPostMultiple() + { + asio::io_context ioc; + capy::executor ex = wrap_executor(ioc.get_executor()); + + int count = 0; + ex.post([&count]{ ++count; }); + ex.post([&count]{ ++count; }); + ex.post([&count]{ ++count; }); + + BOOST_TEST_EQ(count, 0); + test::run(ioc); + BOOST_TEST_EQ(count, 3); + } + + void + testPostWithCapture() + { + asio::io_context ioc; + capy::executor ex = wrap_executor(ioc.get_executor()); + + int result = 0; + int a = 10, b = 20; + ex.post([&result, a, b]{ result = a + b; }); + + test::run(ioc); + BOOST_TEST_EQ(result, 30); + } + + void + testPostWithMoveOnlyCapture() + { + struct callable + { + int& result; + std::unique_ptr ptr; + + void operator()() + { + result = *ptr; + } + }; + + asio::io_context ioc; + capy::executor ex = wrap_executor(ioc.get_executor()); + + int result = 0; + std::unique_ptr ptr(new int(42)); + ex.post(callable{result, std::move(ptr)}); + + test::run(ioc); + BOOST_TEST_EQ(result, 42); + } + + void + testCopyExecutor() + { + asio::io_context ioc; + capy::executor exec1 = wrap_executor(ioc.get_executor()); + capy::executor exec2 = exec1; + + int count = 0; + exec1.post([&count]{ ++count; }); + exec2.post([&count]{ ++count; }); + + test::run(ioc); + BOOST_TEST_EQ(count, 2); + } + + void + testMoveExecutor() + { + asio::io_context ioc; + capy::executor exec1 = wrap_executor(ioc.get_executor()); + capy::executor exec2 = std::move(exec1); + + bool called = false; + exec2.post([&called]{ called = true; }); + + test::run(ioc); + BOOST_TEST(called); + } + + void + testWithStrand() + { + asio::io_context ioc; + asio::strand strand( + ioc.get_executor()); + capy::executor ex = wrap_executor(strand); + + int count = 0; + ex.post([&count]{ ++count; }); + ex.post([&count]{ ++count; }); + + test::run(ioc); + BOOST_TEST_EQ(count, 2); + } + + void + testAssociatedAllocator() + { + asio::io_context ioc; + tracking_allocator alloc; + + executor_with_allocator< + asio::io_context::executor_type, + tracking_allocator> wrapped_exec( + ioc.get_executor(), alloc); + + capy::executor ex = wrap_executor(wrapped_exec); + + bool called = false; + ex.post([&called]{ called = true; }); + + // Before running, allocation should have happened + BOOST_TEST_GT(*alloc.alloc_count, 0); + + test::run(ioc); + BOOST_TEST(called); + + // After running, deallocation should have happened + BOOST_TEST_GT(*alloc.dealloc_count, 0); + BOOST_TEST_EQ(*alloc.alloc_count, *alloc.dealloc_count); + } + + void + testAsyncPostNonVoid() + { + asio::io_context ioc; + capy::executor ex = wrap_executor(ioc.get_executor()); + + int result = 0; + bool handler_called = false; + + ex.submit( + []{ return 42; }, + [&](system::result r) + { + handler_called = true; + if(r.has_value()) + result = r.value(); + }); + + test::run(ioc); + BOOST_TEST(handler_called); + BOOST_TEST_EQ(result, 42); + } + + void + testAsyncPostVoid() + { + asio::io_context ioc; + capy::executor ex = wrap_executor(ioc.get_executor()); + + bool work_called = false; + bool handler_called = false; + + ex.submit( + [&work_called]{ work_called = true; }, + [&handler_called](system::result) + { + handler_called = true; + }); + + test::run(ioc); + BOOST_TEST(work_called); + BOOST_TEST(handler_called); + } + + void + testAsyncPostException() + { + asio::io_context ioc; + capy::executor ex = wrap_executor(ioc.get_executor()); + + bool handler_called = false; + bool got_exception = false; + + ex.submit( + []() -> int { throw std::runtime_error("test"); }, + [&](system::result r) + { + handler_called = true; + if(r.has_error()) + got_exception = true; + }); + + test::run(ioc); + BOOST_TEST(handler_called); + BOOST_TEST(got_exception); + } + + // Test that mimics route_params storage pattern + void + testStoredInStruct() + { + struct params + { + capy::executor ex; + }; + + asio::io_context ioc; + params p; + p.ex = wrap_executor(ioc.get_executor()); + + bool called = false; + p.ex.post([&called]{ called = true; }); + + test::run(ioc); + BOOST_TEST(called); + } + + // Test executor survives struct move + void + testStoredInStructAfterMove() + { + struct params + { + capy::executor ex; + int dummy = 0; + }; + + asio::io_context ioc; + params p1; + p1.ex = wrap_executor(ioc.get_executor()); + + // Move the struct + params p2 = std::move(p1); + + bool called = false; + p2.ex.post([&called]{ called = true; }); + + test::run(ioc); + BOOST_TEST(called); + } + + // Test executor assignment (not construction) + void + testAssignment() + { + asio::io_context ioc; + capy::executor ex; + BOOST_TEST(!static_cast(ex)); + + ex = wrap_executor(ioc.get_executor()); + BOOST_TEST(static_cast(ex)); + + bool called = false; + ex.post([&called]{ called = true; }); + + test::run(ioc); + BOOST_TEST(called); + } + + // Test multiple assignments + void + testReassignment() + { + asio::io_context ioc1; + asio::io_context ioc2; + + capy::executor ex = wrap_executor(ioc1.get_executor()); + + bool called1 = false; + ex.post([&called1]{ called1 = true; }); + test::run(ioc1); + BOOST_TEST(called1); + + // Reassign to different executor + ex = wrap_executor(ioc2.get_executor()); + + bool called2 = false; + ex.post([&called2]{ called2 = true; }); + test::run(ioc2); + BOOST_TEST(called2); + } + + // Test that the underlying asio executor is valid + void + testUnderlyingExecutorValid() + { + asio::io_context ioc; + auto asio_exec = ioc.get_executor(); + + // Wrap it + capy::executor ex = wrap_executor(asio_exec); + + // Post work multiple times to stress test + int count = 0; + for(int i = 0; i < 100; ++i) + { + ex.post([&count]{ ++count; }); + } + + test::run(ioc); + BOOST_TEST_EQ(count, 100); + } + + // Test wrap_executor with temporary executor + void + testWrapTemporary() + { + asio::io_context ioc; + + // Wrap a temporary - this is how it's used in http_stream + capy::executor ex = wrap_executor(ioc.get_executor()); + + bool called = false; + ex.post([&called]{ called = true; }); + + test::run(ioc); + BOOST_TEST(called); + } + + // Test that asio_executor_impl is properly initialized + void + testExecutorImplInitialized() + { + asio::io_context ioc; + + // Create the wrapper directly to test initialization + detail::asio_executor_impl impl( + ioc.get_executor()); + + // The impl should be usable - create work and submit it + struct test_work : capy::executor::work + { + bool& called; + explicit test_work(bool& c) : called(c) {} + void invoke() override { called = true; } + }; + + bool called = false; + + // Allocate, construct, and submit work + void* storage = capy::executor::access::allocate( + impl, sizeof(test_work), alignof(test_work)); + test_work* w = ::new(storage) test_work(called); + capy::executor::access::submit(impl, w); + + test::run(ioc); + BOOST_TEST(called); + } + + // Test that moved asio_executor_impl is valid + void + testExecutorImplAfterMove() + { + asio::io_context ioc; + + detail::asio_executor_impl impl1( + ioc.get_executor()); + + // Move the impl + auto impl2 = std::move(impl1); + + struct test_work : capy::executor::work + { + bool& called; + explicit test_work(bool& c) : called(c) {} + void invoke() override { called = true; } + }; + + bool called = false; + void* storage = capy::executor::access::allocate( + impl2, sizeof(test_work), alignof(test_work)); + test_work* w = ::new(storage) test_work(called); + capy::executor::access::submit(impl2, w); + + test::run(ioc); + BOOST_TEST(called); + } + + void + run() + { + testWrapExecutor(); + testPostLambda(); + testPostMultiple(); + testPostWithCapture(); + testPostWithMoveOnlyCapture(); + testCopyExecutor(); + testMoveExecutor(); + testWithStrand(); + testAssociatedAllocator(); + testAsyncPostNonVoid(); + testAsyncPostVoid(); + testAsyncPostException(); + testStoredInStruct(); + testStoredInStructAfterMove(); + testAssignment(); + testReassignment(); + testUnderlyingExecutorValid(); + testWrapTemporary(); + testExecutorImplInitialized(); + testExecutorImplAfterMove(); + } +}; + +TEST_SUITE( + wrap_executor_test, + "boost.beast2.wrap_executor"); + +} // beast2 +} // boost +