From 08544d1874b0c2508297e5c33f4b462dc6c2d80b Mon Sep 17 00:00:00 2001 From: terrakuh Date: Mon, 3 Jan 2022 23:44:55 +0000 Subject: [PATCH] Initial commit --- .clang-format | 65 +++++++++++ .cmake-format.json | 7 ++ .devcontainer/Dockerfile | 4 + .devcontainer/devcontainer.json | 19 ++++ .gitignore | 1 + .vscode/settings.json | 94 ++++++++++++++++ CMakeLists.txt | 66 +++++++++++ LICENSE | 29 +++++ README.md | 39 +++++++ curlio-config.cmake.in | 11 ++ curlio/CMakeLists.txt | 20 ++++ curlio/detail/function.hpp | 80 ++++++++++++++ curlio/detail/mover.hpp | 38 +++++++ curlio/error.hpp | 94 ++++++++++++++++ curlio/request.hpp | 163 +++++++++++++++++++++++++++ curlio/session.hpp | 189 ++++++++++++++++++++++++++++++++ examples/CMakeLists.txt | 13 +++ examples/playground.cpp | 39 +++++++ 18 files changed, 971 insertions(+) create mode 100644 .clang-format create mode 100644 .cmake-format.json create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 curlio-config.cmake.in create mode 100644 curlio/CMakeLists.txt create mode 100644 curlio/detail/function.hpp create mode 100644 curlio/detail/mover.hpp create mode 100644 curlio/error.hpp create mode 100644 curlio/request.hpp create mode 100644 curlio/session.hpp create mode 100644 examples/CMakeLists.txt create mode 100644 examples/playground.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..fa809c0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +AccessModifierOffset: '-2' +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: 'true' +AlignConsecutiveDeclarations: 'false' +AlignEscapedNewlines: Right +AlignOperands: 'true' +AlignTrailingComments: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'false' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortFunctionsOnASingleLine: 'true' +AllowShortLoopsOnASingleLine: 'true' +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: 'false' +AlwaysBreakTemplateDeclarations: 'Yes' +BinPackArguments: 'true' +BinPackParameters: 'true' +BreakAfterJavaFieldAnnotations: 'true' +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Mozilla +BreakBeforeTernaryOperators: 'true' +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: 'true' +ColumnLimit: '110' +CompactNamespaces: 'false' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'false' +ContinuationIndentWidth: '2' +Cpp11BracedListStyle: 'false' +DerivePointerAlignment: 'false' +FixNamespaceComments: 'true' +IncludeBlocks: Regroup +IndentCaseLabels: 'false' +IndentPPDirectives: AfterHash +IndentWidth: '2' +IndentWrappedFunctionNames: 'true' +JavaScriptQuotes: Leave +JavaScriptWrapImports: 'true' +KeepEmptyLinesAtTheStartOfBlocks: 'false' +MaxEmptyLinesToKeep: '1' +NamespaceIndentation: None +PointerAlignment: Left +ReflowComments: 'true' +SortIncludes: 'true' +SortUsingDeclarations: 'true' +SpaceAfterCStyleCast: 'true' +SpaceAfterTemplateKeyword: 'false' +SpaceBeforeAssignmentOperators: 'true' +SpaceBeforeCpp11BracedList: 'false' +SpaceBeforeCtorInitializerColon: 'true' +SpaceBeforeInheritanceColon: 'true' +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: 'true' +SpaceInEmptyParentheses: 'false' +SpacesInAngles: 'false' +SpacesInCStyleCastParentheses: 'false' +SpacesInContainerLiterals: 'false' +SpacesInParentheses: 'false' +SpacesInSquareBrackets: 'false' +Standard: Cpp11 +TabWidth: '2' +UseTab: ForIndentation + +... diff --git a/.cmake-format.json b/.cmake-format.json new file mode 100644 index 0000000..b702ae6 --- /dev/null +++ b/.cmake-format.json @@ -0,0 +1,7 @@ +{ + "format": { + "line_width": 110, + "dangle_parens": true, + "dangle_align": "prefix" + } +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..c0f1476 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,4 @@ +FROM archlinux + +RUN pacman -Syq --noconfirm python-pip clang git git-lfs openssh cmake ninja gcc gdb nodejs yarn +RUN pip install cmakelang diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..86eb882 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +{ + "name": "C++ Backend", + "build": { + "dockerfile": "Dockerfile" + }, + "settings": {}, + "extensions": [ + "ms-vscode.cpptools", + "twxs.cmake", + "ms-vscode.cmake-tools", + "cheshirekow.cmake-format", + "albert.TabOut", + "gruntfuggly.todo-tree" + ], + "runArgs": [ + "--network=host", + "--cap-add=CAP_SYS_PTRACE" + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d58534f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,94 @@ +{ + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "files.associations": { + "*.ipp": "cpp", + "any": "cpp", + "array": "cpp", + "atomic": "cpp", + "strstream": "cpp", + "barrier": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "cfenv": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "cinttypes": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "codecvt": "cpp", + "compare": "cpp", + "complex": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "coroutine": "cpp", + "csetjmp": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "forward_list": "cpp", + "list": "cpp", + "map": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "source_location": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "rope": "cpp", + "slist": "cpp", + "fstream": "cpp", + "future": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "latch": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "ranges": "cpp", + "scoped_allocator": "cpp", + "semaphore": "cpp", + "shared_mutex": "cpp", + "span": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "syncstream": "cpp", + "thread": "cpp", + "typeindex": "cpp", + "typeinfo": "cpp", + "valarray": "cpp", + "variant": "cpp" + } +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..af078f0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,66 @@ +cmake_minimum_required(VERSION 3.16) + +project( + curlio + VERSION 0.1.0 + DESCRIPTION "The simple glue for cURL and Boost.ASIO" + HOMEPAGE_URL "https://github.com/terrakuh/curlio" + LANGUAGES CXX +) + +option(CURLIO_BUILD_EXAMPLES "The example programs." OFF) + +find_package(CURL REQUIRED) +find_package(Threads REQUIRED) + +find_package(Boost) +if(NOT Boost_FOUND) + message(STATUS "Boost not found. Fetching from jfrog.io") + include(FetchContent) + FetchContent_Declare( + Boost + URL https://boostorg.jfrog.io/artifactory/main/release/1.78.0/source/boost_1_78_0.tar.gz + URL_HASH SHA256=94ced8b72956591c4775ae2207a9763d3600b30d9d7446562c552f0a14a63be7 + ) + FetchContent_MakeAvailable(Boost) + FetchContent_GetProperties(Boost SOURCE_DIR Boost_INCLUDE_DIR) + find_package(Boost REQUIRED) +endif() + +add_subdirectory(curlio) + +if(CURLIO_BUILD_EXAMPLES) + add_subdirectory(examples) +endif() + +# Install +include(CMakePackageConfigHelpers) +configure_package_config_file( + ${PROJECT_NAME}-config.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake" + INSTALL_DESTINATION "${LIBRARY_INSTALL_DIR}/cmake/${PROJECT_NAME}" +) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config-version.cmake" + DESTINATION lib/cmake/${PROJECT_NAME} +) + +# CPack +set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) +set(CPACK_PACKAGE_CONTACT "Yunus Ayar") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}") +set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}") +set(CPACK_PACKAGING_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") +set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") +set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) +set(CPACK_GENERATOR DEB TGZ) +set(CPACK_DEBIAN_PACKAGE_DEPENDS "") +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) + +include(CPack) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a1d81a2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, Yunus Ayar +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..897c928 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# cULRio +Simple header-only **Boost.ASIO** wrapper. + +## Example + +```cpp +curlio::Session session{ service.get_executor() }; +curlio::Request req; +req.set_url("https://example.com"); +curl_easy_setopt(req.native_handle(), CURLOPT_USERAGENT, "curl/7.80.0"); + +session.start(req); +while (true) { + char buf[4096]; + auto [ec, n] = co_await req.async_read_some(buffer(buf), use_nothrow_awaitable); + if (ec == error::eof) { + break; + } + std::cout.write(buf, n); +} +co_await req.async_wait(use_awaitable); +co_return; +``` + +## Installation + +```sh +git clone https://github.com/terrakuh/curlio.git +cmake -S curlio -B curlio/build +cmake --install curlio/build +``` + +And then in your `CMakeLists.txt`: + +```cmake +find_package(curlio REQUIRED) + +target_link_libraries(my-target PRIVATE curlio::curlio) +``` diff --git a/curlio-config.cmake.in b/curlio-config.cmake.in new file mode 100644 index 0000000..9293292 --- /dev/null +++ b/curlio-config.cmake.in @@ -0,0 +1,11 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(CURL REQUIRED) +find_dependency(Boost REQUIRED) +find_dependency(Threads REQUIRED) + +if(NOT TARGET curlio::curlio) + include("${CMAKE_CURRENT_LIST_DIR}/curlio-targets.cmake") + set(curlio_FOUND TRUE) +endif() diff --git a/curlio/CMakeLists.txt b/curlio/CMakeLists.txt new file mode 100644 index 0000000..8158fd3 --- /dev/null +++ b/curlio/CMakeLists.txt @@ -0,0 +1,20 @@ +file(GLOB_RECURSE sources "${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp") + +add_library(curlio INTERFACE ${sources}) +add_library(curlio::curlio ALIAS curlio) +target_link_libraries(curlio INTERFACE CURL::libcurl Threads::Threads Boost::boost) +target_include_directories(curlio BEFORE INTERFACE "$") + +install(TARGETS curlio EXPORT ${PROJECT_NAME}-targets) +install( + EXPORT ${PROJECT_NAME}-targets + DESTINATION lib/cmake/${PROJECT_NAME} + NAMESPACE ${PROJECT_NAME}:: + EXPORT_LINK_INTERFACE_LIBRARIES +) +install( + DIRECTORY . + DESTINATION include/${PROJECT_NAME} + FILES_MATCHING + PATTERN "*.hpp" +) diff --git a/curlio/detail/function.hpp b/curlio/detail/function.hpp new file mode 100644 index 0000000..824da26 --- /dev/null +++ b/curlio/detail/function.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +namespace curlio::detail { + +template +class Function; + +template +class Invoker +{ +public: + virtual ~Invoker() = default; + virtual Return invoke(Arguments... arguments) = 0; +}; + +template +class Functor_invoker : public Invoker +{ +public: + Functor_invoker(const Functor& functor) : _functor{ functor } + {} + Functor_invoker(Functor&& functor) : _functor{ std::move(functor) } + {} + Return invoke(Arguments... arguments) override + { + return _functor(std::forward(arguments)...); + } + +private: + Functor _functor; +}; + +template +class Function +{ +public: + Function() = default; + Function(const Function& copy) = delete; + Function(Function&& move) : _invoker{ std::move(move._invoker) } + {} + template + Function(Functor&& functor) + : _invoker{ std::make_unique::type, Return, Arguments...>>( + std::forward(functor)) } + {} + void reset() + { + _invoker = nullptr; + } + Return operator()(Arguments... arguments) + { + return _invoker->invoke(std::forward(arguments)...); + } + operator bool() const noexcept + { + return _invoker != nullptr; + } + Function& operator=(const Function& copy) = delete; + Function& operator =(Function&& move) + { + _invoker = std::move(move._invoker); + return *this; + } + template + Function& operator=(Functor&& functor) + { + _invoker = std::make_unique::type, Return, Arguments...>>( + std::forward(functor)); + return *this; + } + +private: + std::unique_ptr> _invoker; +}; + +} // namespace curlio::detail diff --git a/curlio/detail/mover.hpp b/curlio/detail/mover.hpp new file mode 100644 index 0000000..53e0183 --- /dev/null +++ b/curlio/detail/mover.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +namespace curlio::detail { + +template +class Mover +{ +public: + Mover() = default; + Mover(Mover&& move) : _value{ std::move(move._value) } { move._value = Type{}; } + + Type& get() noexcept { return _value; } + const Type& get() const noexcept { return _value; } + operator Type&() noexcept { return get(); } + operator const Type&() const noexcept { return get(); } + Mover& operator=(const Type& value) + { + _value = value; + return *this; + } + Mover& operator=(Type&& value) + { + _value = std::exchange(value, Type{}); + return *this; + } + Mover& operator=(Mover&& move) + { + _value = std::exchange(move._value, Type{}); + return *this; + } + +private: + Type _value{}; +}; + +} // namespace curlio::detail diff --git a/curlio/error.hpp b/curlio/error.hpp new file mode 100644 index 0000000..2694978 --- /dev/null +++ b/curlio/error.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include + +namespace curlio { + +enum class Code +{ + success, + + multiple_reads, + request_in_use, +}; + +enum class Condition +{ + success, + usage, +}; + +std::error_condition make_error_condition(Condition condition) noexcept; + +inline const std::error_category& code_category() noexcept +{ + static class : public std::error_category + { + public: + const char* name() const noexcept override { return "curlio"; } + std::error_condition default_error_condition(int code) const noexcept override + { + if (code == 0) { + return make_error_condition(Condition::success); + } else if (code >= 1 && code < 50) { + return make_error_condition(Condition::usage); + } + return error_category::default_error_condition(code); + } + std::string message(int ec) const override + { + switch (static_cast(ec)) { + case Code::success: return "success"; + + case Code::multiple_reads: return "multiple read operations not allowed"; + case Code::request_in_use: return "request is already in use"; + + default: return "(unrecognized error code)"; + } + } + } category; + return category; +} + +inline const std::error_category& condition_category() noexcept +{ + static class : public std::error_category + { + public: + const char* name() const noexcept override { return "curlio"; } + std::string message(int condition) const override + { + switch (static_cast(condition)) { + case Condition::success: return "success"; + case Condition::usage: return "usage"; + default: return "(unrecognized error condition)"; + } + } + } category; + return category; +} + +inline std::error_code make_error_code(Code code) noexcept +{ + return { static_cast(code), code_category() }; +} + +inline std::error_condition make_error_condition(Condition condition) noexcept +{ + return { static_cast(condition), condition_category() }; +} + +} // namespace curlio + +namespace std { + +template<> +struct is_error_code_enum : true_type +{}; + +template<> +struct is_error_condition_enum : true_type +{}; + +} // namespace std diff --git a/curlio/request.hpp b/curlio/request.hpp new file mode 100644 index 0000000..9dcbd33 --- /dev/null +++ b/curlio/request.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include "detail/function.hpp" +#include "detail/mover.hpp" +#include "error.hpp" + +#include +#include +#include +#include + +namespace curlio { + +class Session; + +class Request +{ +public: + /// This constructor assumes ownership of the provided handle. + Request(CURL* handle = curl_easy_init()) noexcept; + Request(Request&& move) = default; + ~Request() noexcept; + + void set_url(const char* url) { curl_easy_setopt(_handle, CURLOPT_URL, url); } + bool is_valid() const noexcept { return _handle != nullptr; } + CURL* native_handle() noexcept { return _handle; } + /// Waits until the request is complete. Data must be read before this function. + template + auto async_wait(Token&& token); + template + auto async_read_some(const Mutable_buffer_sequence& buffers, Token&& token); + boost::asio::any_io_executor get_executor() noexcept { return _executor; } + void swap(Request& other) noexcept; + Request& operator=(Request&& move) = default; + +private: + friend Session; + + /// Set to the session's executor. + boost::asio::any_io_executor _executor; + /// This handler is set when an asynchronous action waits for data. + detail::Function _write_handler; + int _pause_mask = 0; + detail::Mover _handle; + /// Stores one `_write_callback` call. + boost::asio::streambuf _input_buffer; + /// This handler is set when an asynchronous action waits for the request to complete. + detail::Function _finish_handler; + bool _finished = false; + + /// Marks this request as finished. + void _finish(); + static std::size_t _write_callback(void* data, std::size_t size, std::size_t count, + void* self_pointer) noexcept; +}; + +Request::Request(CURL* handle) noexcept +{ + _handle = handle; + if (_handle != nullptr) { + curl_easy_setopt(_handle, CURLOPT_WRITEFUNCTION, &Request::_write_callback); + curl_easy_setopt(_handle, CURLOPT_WRITEDATA, this); + curl_easy_setopt(_handle, CURLOPT_PRIVATE, this); + } +} + +Request::~Request() noexcept +{ + if (_handle != nullptr) { + curl_easy_cleanup(_handle); + } +} + +template +auto Request::async_wait(Token&& token) +{ + return boost::asio::async_initiate( + [this](auto handler) { + if (_finished) { + std::move(handler)(boost::system::error_code{}); + } else { + _finish_handler = [this, handler = std::move(handler)]() mutable { + _finish(); + auto executor = boost::asio::get_associated_executor(handler); + boost::asio::post(executor, [handler = std::move(handler)]() mutable { + std::move(handler)(boost::system::error_code{}); + }); + }; + } + }, + token); +} + +template +auto Request::async_read_some(const Mutable_buffer_sequence& buffers, Token&& token) +{ + return boost::asio::async_initiate( + [this, buffers](auto handler) { + // can immediately finish + if (_input_buffer.size() > 0) { + const std::size_t copied = boost::asio::buffer_copy(buffers, _input_buffer.data()); + _input_buffer.consume(copied); + std::move(handler)(boost::system::error_code{}, copied); + } else if (_write_handler) { + std::move(handler)(Code::multiple_reads, 0); + } else if (_finished) { + std::move(handler)(boost::asio::error::eof, 0); + } else { + // set write handler when cURL calls the write callback + _write_handler = [this, buffers, handler = std::move(handler)](boost::system::error_code ec) mutable { + std::size_t copied = 0; + // copy data and finish + if (!ec) { + copied = boost::asio::buffer_copy(buffers, _input_buffer.data()); + _input_buffer.consume(copied); + } + auto executor = boost::asio::get_associated_executor(handler); + boost::asio::post(executor, [handler = std::move(handler), ec, copied]() mutable { + std::move(handler)(ec, copied); + }); + }; + + // TODO check for errors + _pause_mask &= ~CURLPAUSE_RECV; + curl_easy_pause(_handle, _pause_mask); + } + }, + token); +} + +void Request::_finish() +{ + _finished = true; + if (_write_handler) { + _write_handler(boost::asio::error::eof); + _write_handler.reset(); + } + if (_finish_handler) { + _finish_handler(); + _finish_handler.reset(); + } + _executor = {}; +} + +std::size_t Request::_write_callback(void* data, std::size_t size, std::size_t count, + void* self_pointer) noexcept +{ + auto self = static_cast(self_pointer); + + // data is wanted + if (self->_write_handler) { + const std::size_t copied = boost::asio::buffer_copy(self->_input_buffer.prepare(size * count), + boost::asio::buffer(data, size * count)); + self->_input_buffer.commit(copied); + self->_write_handler({}); + self->_write_handler.reset(); + return copied; + } + self->_pause_mask |= CURLPAUSE_RECV; + return CURL_WRITEFUNC_PAUSE; +} + +} // namespace curlio diff --git a/curlio/session.hpp b/curlio/session.hpp new file mode 100644 index 0000000..5a9f98d --- /dev/null +++ b/curlio/session.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include "detail/mover.hpp" +#include "error.hpp" +#include "request.hpp" + +#include +#include +#include + +namespace curlio { + +class Session +{ +public: + Session(boost::asio::any_io_executor executor); + Session(Session&& move) = default; + ~Session() noexcept; + + bool is_valid() const noexcept { return _multi_handle != nullptr; } + /// Starts the request. Make sure all data is read and the request is awaited. + void start(Request& request); + boost::asio::any_io_executor get_executor() noexcept { return _timer.get_executor(); } + Session& operator=(Session&& move) = delete; + +private: + boost::asio::steady_timer _timer; + detail::Mover _multi_handle; + detail::Mover _share_handle; + /// All active connections. + std::map _sockets; + + void _async_wait(boost::asio::ip::tcp::socket& socket, boost::asio::socket_base::wait_type type); + void _clean_finished(); + static int _socket_callback(CURL* handle, curl_socket_t socket, int what, void* self_pointer, + void* socket_pointer); + static int _multi_timer_callback(CURLM* multi, long timeout_ms, void* self_pointer); + static curl_socket_t _open_socket(void* self_pointer, curlsocktype purpose, + struct curl_sockaddr* address) noexcept; + static int _close_socket(void* self_pointer, curl_socket_t socket) noexcept; +}; + +Session::Session(boost::asio::any_io_executor executor) : _timer{ executor } +{ + _multi_handle = curl_multi_init(); + curl_multi_setopt(_multi_handle, CURLMOPT_SOCKETFUNCTION, &_socket_callback); + curl_multi_setopt(_multi_handle, CURLMOPT_SOCKETDATA, this); + curl_multi_setopt(_multi_handle, CURLMOPT_TIMERFUNCTION, &_multi_timer_callback); + curl_multi_setopt(_multi_handle, CURLMOPT_TIMERDATA, this); + + _share_handle = curl_share_init(); + curl_share_setopt(_share_handle, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); + curl_share_setopt(_share_handle, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); +} + +Session::~Session() noexcept +{ + if (_multi_handle != nullptr) { + curl_multi_cleanup(_multi_handle); + } + if (_share_handle != nullptr) { + curl_share_cleanup(_share_handle); + } +} + +void Session::start(Request& request) +{ + if (request._executor) { + throw std::system_error{ Code::request_in_use }; + } + + const auto easy_handle = request.native_handle(); + request._executor = get_executor(); + curl_easy_setopt(easy_handle, CURLOPT_OPENSOCKETFUNCTION, &Session::_open_socket); + curl_easy_setopt(easy_handle, CURLOPT_OPENSOCKETDATA, this); + curl_easy_setopt(easy_handle, CURLOPT_CLOSESOCKETFUNCTION, &Session::_close_socket); + curl_easy_setopt(easy_handle, CURLOPT_CLOSESOCKETDATA, this); + curl_easy_setopt(easy_handle, CURLOPT_SHARE, _share_handle.get()); + + curl_multi_add_handle(_multi_handle, easy_handle); +} + +void Session::_async_wait(boost::asio::ip::tcp::socket& socket, boost::asio::socket_base::wait_type type) +{ + socket.async_wait(type, [this, type, &socket](boost::system::error_code ec) { + if (!ec) { + const auto handle = socket.native_handle(); + int still_running = 0; + curl_multi_socket_action(_multi_handle, handle, + type == boost::asio::socket_base::wait_read ? CURL_POLL_IN : CURL_POLL_OUT, + &still_running); + _clean_finished(); + if (still_running <= 0) { + _timer.cancel(); + } + + if (_sockets.find(handle) != _sockets.end()) { + _async_wait(socket, type); + } + } + }); +} + +void Session::_clean_finished() +{ + CURLMsg* message = nullptr; + int left = 0; + while ((message = curl_multi_info_read(_multi_handle, &left))) { + if (message->msg == CURLMSG_DONE) { + Request* request = nullptr; + curl_easy_getinfo(message->easy_handle, CURLINFO_PRIVATE, &request); + if (request != nullptr) { + request->_finish(); + } + curl_multi_remove_handle(_multi_handle, message->easy_handle); + } + } +} + +int Session::_socket_callback(CURL* handle, curl_socket_t socket, int what, void* self_pointer, + void* socket_pointer) +{ + const auto self = static_cast(self_pointer); + const auto it = self->_sockets.find(socket); + if (it == self->_sockets.end()) { + return 0; + } + + switch (what) { + case CURL_POLL_IN: self->_async_wait(it->second, boost::asio::socket_base::wait_read); break; + case CURL_POLL_OUT: self->_async_wait(it->second, boost::asio::socket_base::wait_write); break; + case CURL_POLL_INOUT: { + self->_async_wait(it->second, boost::asio::socket_base::wait_read); + self->_async_wait(it->second, boost::asio::socket_base::wait_write); + break; + } + case CURL_POLL_REMOVE: it->second.cancel(); break; + } + return 0; +} + +int Session::_multi_timer_callback(CURLM* multi, long timeout_ms, void* self_pointer) +{ + const auto self = static_cast(self_pointer); + + self->_timer.expires_from_now(std::chrono::milliseconds{ timeout_ms }); + if (timeout_ms > 0) { + self->_timer.async_wait([self](boost::system::error_code ec) { + if (!ec) { + int still_running = 0; + curl_multi_socket_action(self->_multi_handle, CURL_SOCKET_TIMEOUT, 0, &still_running); + self->_clean_finished(); + } + }); + } else { + int still_running = 0; + curl_multi_socket_action(self->_multi_handle, CURL_SOCKET_TIMEOUT, 0, &still_running); + self->_clean_finished(); + } + return 0; +} + +curl_socket_t Session::_open_socket(void* self_pointer, curlsocktype purpose, + struct curl_sockaddr* address) noexcept +{ + const auto self = static_cast(self_pointer); + if (purpose == CURLSOCKTYPE_IPCXN) { + if (address->family == AF_INET) { + boost::system::error_code ec; + boost::asio::ip::tcp::socket socket{ self->get_executor() }; + socket.open(boost::asio::ip::tcp::v4(), ec); + if (!ec) { + auto fd = socket.native_handle(); + self->_sockets.insert(std::make_pair(fd, std::move(socket))); + return fd; + } + } + } + return CURL_SOCKET_BAD; +} + +int Session::_close_socket(void* self_pointer, curl_socket_t socket) noexcept +{ + const auto self = static_cast(self_pointer); + self->_sockets.erase(socket); + return 0; +} + +} // namespace curlio diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..68d6887 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,13 @@ +find_package(Threads REQUIRED) + +file(GLOB examples "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +foreach(example ${examples}) + get_filename_component(name "${example}" NAME_WE) + + add_executable(${name} "${example}") + target_link_libraries(${name} PRIVATE curlio::curlio Threads::Threads) + set_target_properties(${name} PROPERTIES CXX_STANDARD 20) + + # target_compile_options(${name} PUBLIC -fsanitize=address -fno-omit-frame-pointer -O1 -fno-optimize-sibling-calls) + # target_link_options(${name} PUBLIC -fsanitize=address) +endforeach() diff --git a/examples/playground.cpp b/examples/playground.cpp new file mode 100644 index 0000000..7bc237c --- /dev/null +++ b/examples/playground.cpp @@ -0,0 +1,39 @@ + +#include +#include +#include + +using namespace boost::asio; + +constexpr auto use_nothrow_awaitable = experimental::as_tuple(use_awaitable); + +int main(int argc, char** argv) +{ + std::string s; + io_service service; + + co_spawn( + service, + [&]() -> awaitable { + curlio::Session session{ service.get_executor() }; + curlio::Request req{}; + req.set_url("https://example.com"); + // curl_easy_setopt(req.native_handle(), CURLOPT_VERBOSE, 1L); + curl_easy_setopt(req.native_handle(), CURLOPT_USERAGENT, "curl/7.80.0"); + + session.start(req); + while (true) { + char buf[4096]; + auto [ec, n] = co_await req.async_read_some(buffer(buf), use_nothrow_awaitable); + if (ec == error::eof) { + break; + } + std::cout.write(buf, n); + } + co_await req.async_wait(use_awaitable); + co_return; + }, + detached); + + service.run(); +}