├── .gitignore ├── .vscode ├── c_cpp_properties.json ├── launch.json └── settings.json ├── LICENSE.md ├── tests ├── delay_and_print.cpp └── ping_pong.cpp ├── demo ├── Makefile ├── bench.cpp ├── http_client.cpp ├── link_cp.cpp ├── threading.cpp ├── echo_server.cpp └── file_server.cpp ├── CMakePresets.json ├── CMakeLists.txt ├── include └── liburing │ ├── sqe_awaitable.hpp │ ├── utils.hpp │ ├── task.hpp │ └── io_service.hpp ├── .github └── workflows │ └── build-and-test.yaml ├── functions.cmake └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /CMakeLists.txt.user 3 | /build 4 | /async 5 | /demo/file_server 6 | /demo/http_client 7 | /demo/link_cp 8 | /demo/threading 9 | /demo/test 10 | /demo/bench 11 | /demo/echo_server 12 | .ccls-cache/ 13 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/usr/bin/clang++", 10 | "cStandard": "c11", 11 | "cppStandard": "c++20", 12 | "intelliSenseMode": "clang-x64", 13 | "compilerArgs": [ 14 | "-fcoroutines-ts", 15 | "-stdlib=libc++" 16 | ] 17 | } 18 | ], 19 | "version": 4 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(lldb) Launch", 9 | "type": "cppdbg", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/demo/test", 12 | "args": [ 13 | "12345" 14 | ], 15 | "cwd": "${workspaceFolder}/demo", 16 | "environment": [ 17 | { 18 | "name": "LSAN_OPTIONS", 19 | "value": "verbosity=1:log_threads=1" 20 | } 21 | ], 22 | "externalConsole": false, 23 | "MIMode": "gdb", 24 | "miDebuggerPath": "/usr/bin/gdb", 25 | "setupCommands": [ 26 | { 27 | "description": "Enable pretty-printing for gdb", 28 | "text": "-enable-pretty-printing", 29 | "ignoreFailures": true 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2020 Carter Li 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /tests/delay_and_print.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include // https://github.com/fmtlib/fmt 3 | 4 | #include 5 | 6 | int main() { 7 | using uio::io_service; 8 | using uio::task; 9 | using uio::panic_on_err; 10 | using uio::dur2ts; 11 | 12 | io_service service; 13 | 14 | service.run([] (io_service& service) -> task<> { 15 | auto delayAndPrint = [&] (int second, uint8_t iflags = 0) -> task<> { 16 | auto ts = dur2ts(std::chrono::seconds(second)); 17 | co_await service.timeout(&ts, iflags) | panic_on_err("timeout", false); 18 | fmt::print("{:%T}: delayed {}s\n", std::chrono::system_clock::now().time_since_epoch(), second); 19 | }; 20 | 21 | fmt::print("in sequence start\n"); 22 | co_await delayAndPrint(1); 23 | co_await delayAndPrint(2); 24 | co_await delayAndPrint(3); 25 | fmt::print("in sequence end, should wait 6s\n\n"); 26 | 27 | fmt::print("io link start\n"); 28 | delayAndPrint(1, IOSQE_IO_HARDLINK); 29 | delayAndPrint(2, IOSQE_IO_HARDLINK); 30 | co_await delayAndPrint(3); 31 | fmt::print("io link end, should wait 6s\n"); 32 | }(service)); 33 | } 34 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | CXX_COMPILER ?= clang++ 2 | MODE ?= DEBUG 3 | 4 | ifeq ($(MODE),DEBUG) 5 | CXXFLAGS ?= -O0 -rdynamic -D_LIBCPP_DEBUG_LEVEL=1 -fno-omit-frame-pointer -fsanitize=address 6 | else 7 | CXXFLAGS ?= -O3 -DNDEBUG -march=native 8 | endif 9 | 10 | ifeq ($(CXX_COMPILER),clang++) 11 | override CXXFLAGS += -fcoroutines-ts -stdlib=libc++ -lc++ -lc++abi 12 | else 13 | override CXXFLAGS += -fcoroutines 14 | endif 15 | 16 | override CXXFLAGS += -g -Wall -std=c++17 -I.. -lfmt -luring -pthread 17 | 18 | all_targets = file_server http_client link_cp threading test bench echo_server 19 | 20 | all: $(all_targets) 21 | 22 | clean: 23 | rm -f $(all_targets) 24 | 25 | help: 26 | @echo 'make [MODE={DEBUG,RELEASE}] [CXX_COMPILER={g++|clang++}]' 27 | 28 | file_server: file_server.cpp ../include/task.hpp ../include/io_service.hpp 29 | $(CXX_COMPILER) ./file_server.cpp -I../include -o file_server $(CXXFLAGS) 30 | 31 | http_client: http_client.cpp ../include/task.hpp ../include/io_service.hpp 32 | $(CXX_COMPILER) ./http_client.cpp -I../include -o http_client $(CXXFLAGS) 33 | 34 | link_cp: link_cp.cpp ../include/task.hpp ../include/io_service.hpp 35 | $(CXX_COMPILER) ./link_cp.cpp -I../include -o link_cp $(CXXFLAGS) 36 | 37 | threading: threading.cpp ../include/task.hpp ../include/io_service.hpp 38 | $(CXX_COMPILER) ./threading.cpp -I../include -o threading $(CXXFLAGS) 39 | 40 | test: test.cpp ../include/task.hpp ../include/io_service.hpp 41 | $(CXX_COMPILER) ./test.cpp -I../include -o test $(CXXFLAGS) 42 | 43 | bench: bench.cpp ../include/task.hpp ../include/io_service.hpp 44 | $(CXX_COMPILER) ./bench.cpp -I../include -o bench $(CXXFLAGS) 45 | 46 | echo_server: echo_server.cpp ../include/task.hpp ../include/io_service.hpp 47 | $(CXX_COMPILER) ./echo_server.cpp -I../include -o echo_server $(CXXFLAGS) 48 | -------------------------------------------------------------------------------- /demo/bench.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include // https://github.com/fmtlib/fmt 4 | 5 | #include 6 | 7 | struct stopwatch { 8 | stopwatch(std::string_view str_): str(str_) {} 9 | ~stopwatch() { 10 | fmt::print("{:<20}{:>12}\n", str, (clock::now() - start).count()); 11 | } 12 | 13 | using clock = std::chrono::high_resolution_clock; 14 | std::string_view str; 15 | clock::time_point start = clock::now(); 16 | }; 17 | 18 | int main() { 19 | using uio::io_service; 20 | using uio::task; 21 | 22 | io_service service; 23 | const auto iteration = 10000000; 24 | 25 | service.run([] (io_service& service) -> task<> { 26 | { 27 | stopwatch sw("service.yield:"); 28 | for (int i = 0; i < iteration; ++i) { 29 | co_await service.yield(); 30 | } 31 | } 32 | { 33 | stopwatch sw("plain IORING_OP_NOP:"); 34 | for (int i = 0; i < iteration; ++i) { 35 | auto* ring = &service.get_handle(); 36 | auto* sqe = io_uring_get_sqe(ring); 37 | io_uring_prep_nop(sqe); 38 | io_uring_submit_and_wait(ring, 1); 39 | 40 | io_uring_cqe *cqe; 41 | io_uring_peek_cqe(ring, &cqe); 42 | (void) cqe->res; 43 | io_uring_cqe_seen(ring, cqe); 44 | } 45 | } 46 | { 47 | stopwatch sw("this_thread::yield:"); 48 | for (int i = 0; i < iteration; ++i) { 49 | std::this_thread::yield(); 50 | } 51 | } 52 | #if defined(__i386__) || defined(__x86_64__) 53 | { 54 | stopwatch sw("pause:"); 55 | for (int i = 0; i < iteration; ++i) { 56 | __builtin_ia32_pause(); 57 | } 58 | } 59 | #endif 60 | }(service)); 61 | } 62 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 19, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "Release", 11 | "displayName": "Configure Release", 12 | "description": "Release Preset", 13 | "binaryDir": "${sourceDir}/build", 14 | "generator": "Ninja", 15 | "cacheVariables": { 16 | "CMAKE_BUILD_TYPE": "Release" 17 | } 18 | }, 19 | { 20 | "name": "Debug", 21 | "displayName": "Configure Debug", 22 | "description": "Debug Preset", 23 | "binaryDir": "${sourceDir}/build", 24 | "generator": "Ninja", 25 | "cacheVariables": { 26 | "CMAKE_BUILD_TYPE": "Debug" 27 | } 28 | }, 29 | { 30 | "name": "Sanitize", 31 | "displayName": "Configure for Address Sanitizer", 32 | "description": "Configure for address sanitizer", 33 | "binaryDir": "${sourceDir}/build", 34 | "generator": "Ninja", 35 | "cacheVariables": { 36 | "CMAKE_C_COMPILER": "clang", 37 | "CMAKE_CXX_COMPILER": "clang++", 38 | "CMAKE_BUILD_TYPE": "Debug", 39 | "ALWAYS_FETCH": "ON", 40 | "CMAKE_CXX_FLAGS": "-stdlib=libc++ -fsanitize=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope" 41 | } 42 | } 43 | ], 44 | "buildPresets": [ 45 | { 46 | "name": "Release", 47 | "configurePreset": "Release", 48 | "configuration": "Release", 49 | "jobs": 8 50 | }, 51 | { 52 | "name": "Debug", 53 | "configurePreset": "Debug", 54 | "configuration": "Debug", 55 | "jobs": 8 56 | }, 57 | { 58 | "name": "Sanitize", 59 | "configurePreset": "Sanitize", 60 | "configuration": "Debug", 61 | "jobs": 8 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) # CMake version check 2 | 3 | project("liburing4cpp") 4 | set(libname "liburing4cpp") 5 | set(CMAKE_CXX_STANDARD 20) 6 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 7 | set(CMAKE_CXX_FLAGS "-fcoroutines") 8 | endif() 9 | add_library("${libname}" INTERFACE) 10 | 11 | ####################### 12 | ## Find dependencies ## 13 | ####################### 14 | # Find a threads library 15 | find_package(Threads REQUIRED) 16 | 17 | # Defines helper functions, like find_or_fetch 18 | include(FetchContent) 19 | include(functions.cmake) 20 | 21 | find_or_fetch( 22 | fmt # Package Name 23 | https://github.com/fmtlib/fmt.git # Repository 24 | master) # Branch (or commit or tag) 25 | 26 | # find_or_fetch adds stuff to a list called remote_dependencies 27 | note("Remote dependencies: ${remote_dependencies}") 28 | FetchContent_MakeAvailable(${remote_dependencies}) 29 | 30 | ################### 31 | ## Configuration ## 32 | ################### 33 | 34 | # This creates a file compile_commands.json in the build directory 35 | # That contains the compile commands used to actually compile the project 36 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 37 | 38 | # Use pthreads as the Threads library, if availible 39 | set(THREADS_PREFER_PTHREAD_FLAG ON) 40 | 41 | ######################## 42 | ## Define the library ## 43 | ######################## 44 | 45 | target_include_directories( 46 | ${libname} 47 | INTERFACE 48 | include/) 49 | 50 | # The library has fmt, uring, and Threads::Threads as dependencies 51 | target_link_libraries( 52 | ${libname} 53 | INTERFACE 54 | fmt uring Threads::Threads) 55 | 56 | ############################################## 57 | ## Build demo when the project is top level ## 58 | ############################################## 59 | 60 | # NB: If this project is included as a dependency as another project, 61 | # Then demo won't get built 62 | 63 | if (PROJECT_IS_TOP_LEVEL) 64 | note("${PROJECT_NAME} is top level. Building demo projects") 65 | # Target all the source files in demo 66 | add_source_dir("demo" ${libname}) 67 | 68 | include(CTest) 69 | 70 | add_test_dir("tests" ${libname}) 71 | endif() 72 | -------------------------------------------------------------------------------- /demo/http_client.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include // https://github.com/fmtlib/fmt 13 | 14 | #include 15 | 16 | uio::task<> start_work(uio::io_service& service, const char* hostname) { 17 | addrinfo hints = { 18 | .ai_family = AF_UNSPEC, 19 | .ai_socktype = SOCK_STREAM, 20 | }, *addrs; 21 | if (int ret = getaddrinfo(hostname, "http", &hints, &addrs); ret < 0) { 22 | fmt::print(stderr, "getaddrinfo({}): {}\n", hostname, gai_strerror(ret)); 23 | throw std::runtime_error("getaddrinfo"); 24 | } 25 | uio::on_scope_exit freeaddr([=]() { freeaddrinfo(addrs); }); 26 | 27 | for (struct addrinfo *addr = addrs; addr; addr = addr->ai_next) { 28 | int clientfd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol) | uio::panic_on_err("socket creation", true); 29 | uio::on_scope_exit closesock([&]() { service.close(clientfd); }); 30 | 31 | if (co_await service.connect(clientfd, addr->ai_addr, addr->ai_addrlen) < 0) continue; 32 | 33 | auto header = fmt::format("GET / HTTP/1.0\r\nHost: {}\r\nAccept: */*\r\n\r\n", hostname); 34 | co_await service.send(clientfd, header.data(), header.size(), MSG_NOSIGNAL) | uio::panic_on_err("send", false); 35 | 36 | std::array buffer; 37 | int res; 38 | for (;;) { 39 | res = co_await service.recv(clientfd, buffer.data(), buffer.size(), MSG_NOSIGNAL | MSG_MORE) | uio::panic_on_err("recv", false); 40 | if (res == 0) break; 41 | co_await service.write(STDOUT_FILENO, buffer.data(), unsigned(res), 0) | uio::panic_on_err("write", false); 42 | } 43 | 44 | co_return; 45 | } 46 | 47 | throw std::runtime_error("Unable to connect any resolved server"); 48 | } 49 | 50 | int main(int argc, char* argv[]) { 51 | if (argc != 2) { 52 | fmt::print("Usage: {} \n", argv[0]); 53 | return 1; 54 | } 55 | 56 | uio::io_service service; 57 | 58 | // Start main coroutine ( for co_await ) 59 | service.run(start_work(service, argv[1])); 60 | } 61 | -------------------------------------------------------------------------------- /include/liburing/sqe_awaitable.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace uio { 11 | struct resolver { 12 | virtual void resolve(int result) noexcept = 0; 13 | }; 14 | 15 | struct resume_resolver final: resolver { 16 | friend struct sqe_awaitable; 17 | 18 | void resolve(int result) noexcept override { 19 | this->result = result; 20 | handle.resume(); 21 | } 22 | 23 | private: 24 | std::coroutine_handle<> handle; 25 | int result = 0; 26 | }; 27 | static_assert(std::is_trivially_destructible_v); 28 | 29 | struct deferred_resolver final: resolver { 30 | void resolve(int result) noexcept override { 31 | this->result = result; 32 | } 33 | 34 | #ifndef NDEBUG 35 | ~deferred_resolver() { 36 | assert(!!result && "deferred_resolver is destructed before it's resolved"); 37 | } 38 | #endif 39 | 40 | std::optional result; 41 | }; 42 | 43 | struct callback_resolver final: resolver { 44 | callback_resolver(std::function&& cb): cb(std::move(cb)) {} 45 | 46 | void resolve(int result) noexcept override { 47 | this->cb(result); 48 | delete this; 49 | } 50 | 51 | private: 52 | std::function cb; 53 | }; 54 | 55 | struct sqe_awaitable { 56 | // TODO: use cancel_token to implement cancellation 57 | sqe_awaitable(io_uring_sqe* sqe) noexcept: sqe(sqe) {} 58 | 59 | // User MUST keep resolver alive before the operation is finished 60 | void set_deferred(deferred_resolver& resolver) { 61 | io_uring_sqe_set_data(sqe, &resolver); 62 | } 63 | 64 | void set_callback(std::function cb) { 65 | io_uring_sqe_set_data(sqe, new callback_resolver(std::move(cb))); 66 | } 67 | 68 | auto operator co_await() { 69 | struct await_sqe { 70 | resume_resolver resolver {}; 71 | io_uring_sqe* sqe; 72 | 73 | await_sqe(io_uring_sqe* sqe): sqe(sqe) {} 74 | 75 | constexpr bool await_ready() const noexcept { return false; } 76 | 77 | void await_suspend(std::coroutine_handle<> handle) noexcept { 78 | resolver.handle = handle; 79 | io_uring_sqe_set_data(sqe, &resolver); 80 | } 81 | 82 | constexpr int await_resume() const noexcept { return resolver.result; } 83 | }; 84 | 85 | return await_sqe(sqe); 86 | } 87 | 88 | private: 89 | io_uring_sqe* sqe; 90 | }; 91 | 92 | } // namespace uio 93 | -------------------------------------------------------------------------------- /demo/link_cp.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #define BS (1024) 10 | 11 | static off_t get_file_size(int fd) { 12 | struct stat st; 13 | 14 | fstat(fd, &st) | uio::panic_on_err("fstat", true); 15 | 16 | if (__builtin_expect(S_ISREG(st.st_mode), true)) { 17 | return st.st_size; 18 | } 19 | 20 | if (S_ISBLK(st.st_mode)) { 21 | unsigned long long bytes; 22 | ioctl(fd, BLKGETSIZE64, &bytes) | uio::panic_on_err("ioctl", true); 23 | return bytes; 24 | } 25 | 26 | throw std::runtime_error("Unsupported file type"); 27 | } 28 | 29 | uio::task<> copy_file(uio::io_service& service, off_t insize) { 30 | using uio::on_scope_exit; 31 | using uio::to_iov; 32 | using uio::panic_on_err; 33 | 34 | std::vector buf(BS, '\0'); 35 | service.register_buffers({ to_iov(buf.data(), buf.size()) }); 36 | on_scope_exit unreg_bufs([&]() { service.unregister_buffers(); }); 37 | 38 | off_t offset = 0; 39 | for (; offset < insize - BS; offset += BS) { 40 | service.read_fixed(0, buf.data(), buf.size(), offset, 0, IOSQE_FIXED_FILE | IOSQE_IO_LINK) | panic_on_err("read_fixed(1)", false); 41 | service.write_fixed(1, buf.data(), buf.size(), offset, 0, IOSQE_FIXED_FILE | IOSQE_IO_LINK) | panic_on_err("write_fixed(1)", false); 42 | } 43 | 44 | int left = insize - offset; 45 | if (left) 46 | { 47 | service.read_fixed(0, buf.data(), left, offset, 0, IOSQE_FIXED_FILE | IOSQE_IO_LINK) | panic_on_err("read_fixed(2)", false); 48 | service.write_fixed(1, buf.data(), left, offset, 0, IOSQE_FIXED_FILE | IOSQE_IO_LINK) | panic_on_err("write_fixed(2)", false); 49 | } 50 | co_await service.fsync(1, 0, IOSQE_FIXED_FILE); 51 | } 52 | 53 | int main(int argc, char *argv[]) { 54 | using uio::panic_on_err; 55 | using uio::on_scope_exit; 56 | using uio::io_service; 57 | 58 | if (argc < 3) { 59 | printf("%s: infile outfile\n", argv[0]); 60 | return 1; 61 | } 62 | 63 | int infd = open(argv[1], O_RDONLY) | panic_on_err("open infile", true); 64 | on_scope_exit close_infd([=]() { close(infd); }); 65 | 66 | int outfd = creat(argv[2], 0644) | panic_on_err("creat outfile", true); 67 | on_scope_exit close_outfd([=]() { close(outfd); }); 68 | 69 | off_t insize = get_file_size(infd); 70 | io_service service; 71 | service.register_files({ infd, outfd }); 72 | on_scope_exit unreg_file([&]() { service.unregister_files(); }); 73 | 74 | service.run(copy_file(service, insize)); 75 | } 76 | -------------------------------------------------------------------------------- /include/liburing/utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace uio { 8 | /** Fill an iovec struct using buf & size */ 9 | constexpr inline iovec to_iov(void *buf, size_t size) noexcept { 10 | return { buf, size }; 11 | } 12 | /** Fill an iovec struct using string view */ 13 | constexpr inline iovec to_iov(std::string_view sv) noexcept { 14 | return to_iov(const_cast(sv.data()), sv.size()); 15 | } 16 | /** Fill an iovec struct using std::array */ 17 | template 18 | constexpr inline iovec to_iov(std::array& array) noexcept { 19 | return to_iov(array.data(), array.size()); 20 | } 21 | 22 | template 23 | struct on_scope_exit { 24 | on_scope_exit(Fn &&fn): _fn(std::move(fn)) {} 25 | ~on_scope_exit() { this->_fn(); } 26 | 27 | private: 28 | Fn _fn; 29 | }; 30 | 31 | [[nodiscard]] 32 | constexpr inline __kernel_timespec dur2ts(std::chrono::nanoseconds dur) noexcept { 33 | auto secs = std::chrono::duration_cast(dur); 34 | dur -= secs; 35 | return { secs.count(), dur.count() }; 36 | } 37 | 38 | /** Convert errno to exception 39 | * @throw std::runtime_error / std::system_error 40 | * @return never 41 | */ 42 | [[noreturn]] 43 | void panic(std::string_view sv, int err) { 44 | #ifndef NDEBUG 45 | // https://stackoverflow.com/questions/77005/how-to-automatically-generate-a-stacktrace-when-my-program-crashes 46 | void *array[32]; 47 | size_t size; 48 | 49 | // get void*'s for all entries on the stack 50 | size = backtrace(array, 32); 51 | 52 | // print out all the frames to stderr 53 | fprintf(stderr, "Error: errno %d:\n", err); 54 | backtrace_symbols_fd(array, size, STDERR_FILENO); 55 | 56 | // __asm__("int $3"); 57 | #endif 58 | 59 | throw std::system_error(err, std::generic_category(), sv.data()); 60 | } 61 | 62 | struct panic_on_err { 63 | panic_on_err(std::string_view _command, bool _use_errno) 64 | : command(_command) 65 | , use_errno(_use_errno) {} 66 | std::string_view command; 67 | bool use_errno; 68 | }; 69 | 70 | inline int operator |(int ret, panic_on_err&& poe) { 71 | if (ret < 0) { 72 | if (poe.use_errno) { 73 | panic(poe.command, errno); 74 | } else { 75 | if (ret != -ETIME) panic(poe.command, -ret); 76 | } 77 | } 78 | return ret; 79 | } 80 | template 81 | inline task operator |(task tret, panic_on_err&& poe) { 82 | co_return (co_await tret) | std::move(poe); 83 | } 84 | inline task operator |(sqe_awaitable tret, panic_on_err&& poe) { 85 | co_return (co_await tret) | std::move(poe); 86 | } 87 | 88 | } // namespace uio 89 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "files.associations": { 4 | ".bootstraprc": "yaml", 5 | ".htmlhintrc": "json", 6 | "chrono": "cpp", 7 | "string_view": "cpp", 8 | "coroutine": "cpp", 9 | "exception": "cpp", 10 | "any": "cpp", 11 | "array": "cpp", 12 | "atomic": "cpp", 13 | "*.tcc": "cpp", 14 | "bitset": "cpp", 15 | "cctype": "cpp", 16 | "clocale": "cpp", 17 | "cmath": "cpp", 18 | "codecvt": "cpp", 19 | "condition_variable": "cpp", 20 | "cstdarg": "cpp", 21 | "cstddef": "cpp", 22 | "cstdint": "cpp", 23 | "cstdio": "cpp", 24 | "cstdlib": "cpp", 25 | "cstring": "cpp", 26 | "ctime": "cpp", 27 | "cwchar": "cpp", 28 | "cwctype": "cpp", 29 | "deque": "cpp", 30 | "list": "cpp", 31 | "unordered_map": "cpp", 32 | "vector": "cpp", 33 | "algorithm": "cpp", 34 | "functional": "cpp", 35 | "iterator": "cpp", 36 | "memory": "cpp", 37 | "memory_resource": "cpp", 38 | "numeric": "cpp", 39 | "optional": "cpp", 40 | "random": "cpp", 41 | "ratio": "cpp", 42 | "string": "cpp", 43 | "system_error": "cpp", 44 | "tuple": "cpp", 45 | "type_traits": "cpp", 46 | "utility": "cpp", 47 | "fstream": "cpp", 48 | "initializer_list": "cpp", 49 | "iomanip": "cpp", 50 | "iosfwd": "cpp", 51 | "iostream": "cpp", 52 | "istream": "cpp", 53 | "limits": "cpp", 54 | "mutex": "cpp", 55 | "new": "cpp", 56 | "ostream": "cpp", 57 | "sstream": "cpp", 58 | "stdexcept": "cpp", 59 | "streambuf": "cpp", 60 | "thread": "cpp", 61 | "cinttypes": "cpp", 62 | "typeinfo": "cpp", 63 | "variant": "cpp", 64 | "bit": "cpp", 65 | "__bit_reference": "cpp", 66 | "__config": "cpp", 67 | "__debug": "cpp", 68 | "__errc": "cpp", 69 | "__functional_base": "cpp", 70 | "__hash_table": "cpp", 71 | "__locale": "cpp", 72 | "__mutex_base": "cpp", 73 | "__node_handle": "cpp", 74 | "__nullptr": "cpp", 75 | "__split_buffer": "cpp", 76 | "__string": "cpp", 77 | "__threading_support": "cpp", 78 | "__tuple": "cpp", 79 | "ios": "cpp", 80 | "locale": "cpp", 81 | "queue": "cpp", 82 | "span": "cpp", 83 | "stack": "cpp", 84 | "*.ipp": "cpp", 85 | "future": "cpp", 86 | "csignal": "cpp", 87 | "map": "cpp", 88 | "__tree": "cpp", 89 | "compare": "cpp", 90 | "__functional_03": "cpp", 91 | "filesystem": "cpp", 92 | "format": "cpp", 93 | "set": "cpp", 94 | "complex": "cpp", 95 | "typeindex": "cpp", 96 | "version": "cpp", 97 | "forward_list": "cpp", 98 | "cassert": "cpp" 99 | }, 100 | "search.exclude": { 101 | ".vscode": true, 102 | "fmt": true, 103 | "README.md" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: CMake 3 | on: [push, pull_request] 4 | 5 | env: 6 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 7 | BUILD_TYPE: Release 8 | 9 | jobs: 10 | build: 11 | # The CMake configure and build commands are platform agnostic and should work equally 12 | # well on Windows or Mac. You can convert this to a matrix build if you need 13 | # cross-platform coverage. 14 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | name: [ gcc-11, clang-14-libc++ ] 21 | 22 | include: 23 | - name: gcc-11 24 | compiler: g++-11 25 | packages: > 26 | g++-11 27 | compiler-flags: "" 28 | 29 | - name: clang-14-libc++ 30 | compiler: clang++-14 31 | packages: > 32 | clang++-14 33 | libc++-14-dev 34 | libc++1-14 35 | libc++abi1-14 36 | libc++abi-14-dev 37 | compiler-flags: "-stdlib=libc++" 38 | 39 | 40 | steps: 41 | - uses: actions/checkout@v3 42 | 43 | - name: Install Compilers 44 | run: | 45 | if [[ "${{matrix.name}}" == "clang-14-libc++" ]]; then \ 46 | wget https://apt.llvm.org/llvm.sh -O /tmp/llvm.sh; \ 47 | sudo bash /tmp/llvm.sh 14; \ 48 | sudo apt-get install -y libc++-14-dev libc++abi-14-dev; \ 49 | else \ 50 | sudo apt-get update -y; \ 51 | sudo apt-get install -y g++-11; \ 52 | fi; 53 | 54 | # Package liburing-dev only exists in ubuntu 20.10 and later, so we have to 55 | # install it manually 56 | - name: Install liburing 57 | working-directory: "/tmp" 58 | run: | 59 | git clone https://github.com/axboe/liburing.git 60 | cd liburing 61 | ./configure 62 | make -j 8 63 | sudo make install 64 | 65 | - name: Configure CMake 66 | working-directory: ${{ github.workspace }} 67 | run: > 68 | cmake 69 | -B build 70 | -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} 71 | -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} 72 | -DCMAKE_CXX_FLAGS="${{ matrix.compiler-flags }}" 73 | 74 | - name: Build 75 | working-directory: ${{ github.workspace }} 76 | # Build your program with the given configuration 77 | run: > 78 | cmake 79 | --build build 80 | --config ${{ env.BUILD_TYPE }} 81 | 82 | 83 | # TODO: create test programs in test/ directory, register these with CTest 84 | - name: Test 85 | working-directory: ${{github.workspace}}/build 86 | # Execute tests defined by the CMake configuration. 87 | # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail 88 | run: ctest -C ${{env.BUILD_TYPE}} 89 | -------------------------------------------------------------------------------- /tests/ping_pong.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | auto ping(uio::io_service& service, int read_fd, int write_fd) -> uio::task<> { 8 | std::string_view msg = "ping!"; 9 | std::string_view expected = "pong!"; 10 | std::array buffer; 11 | 12 | for (int i = 0; i < 20; i++) { 13 | int count = 14 | co_await service.read(read_fd, buffer.data(), buffer.size(), 0) 15 | | uio::panic_on_err("ping: Unable to read from read_fd", false); 16 | 17 | auto recieved = std::string_view(buffer.data(), count); 18 | 19 | fmt::print( 20 | fg(fmt::color::cyan) | fmt::emphasis::bold, 21 | "ping: Recieved {}\n", 22 | recieved); 23 | if (recieved != expected) 24 | uio::panic("Unexpected message", 0); 25 | 26 | co_await service.write(write_fd, msg.data(), msg.size(), 0) 27 | | uio::panic_on_err("ping: Unable to write to write_fd", false); 28 | } 29 | 30 | co_await service.close(write_fd) 31 | | uio::panic_on_err("ping: Unable to close write_fd", false); 32 | 33 | // Check for EOF before exiting 34 | if(0 != co_await service.read(read_fd, buffer.data(), buffer.size(), 0)) { 35 | throw std::runtime_error("pong: Pipe not at EOF like expected"); 36 | } 37 | 38 | co_await service.close(read_fd) 39 | | uio::panic_on_err("ping: Unable to close read_fd", false); 40 | } 41 | 42 | auto pong(uio::io_service& service, int read_fd, int write_fd) -> uio::task<> { 43 | std::string_view msg = "pong!"; 44 | std::string_view expected = "ping!"; 45 | std::array buffer; 46 | 47 | for (int i = 0; i < 20; i++) { 48 | co_await service.write(write_fd, msg.data(), msg.size(), 0) 49 | | uio::panic_on_err("pong: Unable to write to write_fd", false); 50 | 51 | int count = 52 | co_await service.read(read_fd, buffer.data(), buffer.size(), 0) 53 | | uio::panic_on_err("pong: Unable to read from read_fd", false); 54 | 55 | auto recieved = std::string_view(buffer.data(), count); 56 | 57 | fmt::print( 58 | fg(fmt::color::magenta) | fmt::emphasis::bold, 59 | "pong: Recieved {}\n", 60 | recieved); 61 | if (recieved != expected) 62 | uio::panic("Unexpected message", 0); 63 | } 64 | 65 | co_await service.close(write_fd) 66 | | uio::panic_on_err("pong: Unable to close write_fd", false); 67 | 68 | // Check for EOF before exiting 69 | if(0 != co_await service.read(read_fd, buffer.data(), buffer.size(), 0)) { 70 | throw std::runtime_error("pong: Pipe not at EOF like expected"); 71 | } 72 | 73 | co_await service.close(read_fd) 74 | | uio::panic_on_err("pong: Unable to close read_fd", false); 75 | } 76 | 77 | int main() { 78 | using uio::io_service; 79 | using uio::task; 80 | 81 | io_service io; 82 | 83 | std::array p1; 84 | std::array p2; 85 | pipe(p1.data()) | uio::panic_on_err("Unable to open pipe", true); 86 | pipe(p2.data()) | uio::panic_on_err("Unable to open pipe", true); 87 | 88 | // ping reads from p1 and writes to p2 89 | auto t1 = ping(io, p1[0], p2[1]); 90 | // pong writes to p1 and reads from p2 91 | auto t2 = pong(io, p2[0], p1[1]); 92 | 93 | io.run(t1); 94 | io.run(t2); 95 | } 96 | -------------------------------------------------------------------------------- /demo/threading.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | 11 | template 12 | uio::task> invoke(uio::io_service& service, Fn&& fn) noexcept(noexcept(fn())) { 13 | using result_t = std::invoke_result_t; 14 | int efd = ::eventfd(0, EFD_CLOEXEC); 15 | uio::on_scope_exit closefd([=]() { ::close(efd); }); 16 | std::variant< 17 | std::monostate, 18 | std::conditional_t, std::monostate, result_t>, 19 | std::conditional_t 20 | > result; 21 | std::thread([&]() { 22 | uio::on_scope_exit writefd([=]() { ::eventfd_write(efd, 1); }); 23 | try { 24 | if constexpr (std::is_void_v) { 25 | fn(); 26 | } else { 27 | std::atomic_signal_fence(std::memory_order_acquire); 28 | result.template emplace<1>(fn()); 29 | std::atomic_signal_fence(std::memory_order_release); 30 | } 31 | } catch (...) { 32 | if constexpr (!noexcept (fn())) { 33 | std::atomic_signal_fence(std::memory_order_acquire); 34 | result.template emplace<2>(std::current_exception()); 35 | std::atomic_signal_fence(std::memory_order_release); 36 | } else { 37 | __builtin_unreachable(); 38 | } 39 | } 40 | }).detach(); 41 | co_await service.poll(efd, POLLIN); 42 | 43 | if constexpr (!noexcept (fn())) { 44 | if (result.index() == 2) std::rethrow_exception(std::get<2>(result)); 45 | } 46 | 47 | if constexpr (!std::is_void_v) { 48 | co_return std::move(std::get<1>(result)); 49 | } 50 | } 51 | 52 | struct async_mutex { 53 | async_mutex(): efd(::eventfd(1, EFD_CLOEXEC)) {}; 54 | async_mutex(async_mutex&& other): efd(other.efd) { 55 | other.efd = 0; 56 | }; 57 | async_mutex(const async_mutex& other): efd(::dup(other.efd)) {}; 58 | 59 | void lock() { 60 | eventfd_t value = 0; 61 | [[maybe_unused]] int res = eventfd_read(efd, &value); 62 | assert(res > 0 && value == 1); 63 | } 64 | 65 | bool try_lock() { 66 | eventfd_t value = 0; 67 | auto iov = uio::to_iov(&value, sizeof(value)); 68 | auto res = preadv2(efd, &iov, 1, 0, RWF_NOWAIT); 69 | return res > 0; 70 | } 71 | 72 | uio::task<> async_lock(uio::io_service& service) { 73 | eventfd_t value = 0; 74 | [[maybe_unused]] int res = co_await service.read(efd, &value, sizeof(value), 0); 75 | assert(res > 0 && value == 1); 76 | } 77 | 78 | void unlock() { 79 | eventfd_write(efd, 1); 80 | } 81 | 82 | int efd; 83 | }; 84 | 85 | int main() { 86 | uio::io_service service; 87 | using namespace std::chrono_literals; 88 | 89 | service.run([&] () -> uio::task<> { 90 | int efd = eventfd(0, EFD_CLOEXEC | EFD_SEMAPHORE); 91 | eventfd_t v1 = -1, v2 = -1; 92 | invoke(service, [=]() noexcept { 93 | std::this_thread::sleep_for(1s); 94 | eventfd_write(efd, 123); 95 | }); 96 | [[maybe_unused]] auto read1 = service.read(efd, &v1, sizeof(v1), 0); 97 | co_await service.read(efd, &v2, sizeof(v2), 0); 98 | fmt::print("{},{}\n", v1, v2); 99 | }()); 100 | } 101 | -------------------------------------------------------------------------------- /demo/echo_server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include // https://github.com/fmtlib/fmt 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #define USE_SPLICE 0 12 | #define USE_LINK 0 13 | #define USE_POLL 0 14 | 15 | enum { 16 | BUF_SIZE = 512, 17 | MAX_CONN_SIZE = 512, 18 | }; 19 | 20 | int runningCoroutines = 0; 21 | 22 | uio::task<> accept_connection(uio::io_service& service, int serverfd) { 23 | while (int clientfd = co_await service.accept(serverfd, nullptr, nullptr)) { 24 | [](uio::io_service& service, int clientfd) -> uio::task<> { 25 | fmt::print("sockfd {} is accepted; number of running coroutines: {}\n", 26 | clientfd, ++runningCoroutines); 27 | #if USE_SPLICE 28 | int pipefds[2]; 29 | pipe(pipefds) | panic_on_err("pipe", true); 30 | on_scope_exit([&] { close(pipefds[0]); close(pipefds[1]); }); 31 | #else 32 | std::vector buf(BUF_SIZE); 33 | #endif 34 | while (true) { 35 | #if USE_POLL 36 | # if USE_LINK 37 | service.poll(clientfd, POLLIN, IOSQE_IO_LINK); 38 | # else 39 | co_await service.poll(clientfd, POLLIN); 40 | # endif 41 | #endif 42 | #if USE_SPLICE 43 | # if USE_LINK 44 | service.splice(clientfd, -1, pipefds[1], -1, -1, SPLICE_F_MOVE, IOSQE_IO_HARDLINK); 45 | int r = co_await service.splice(pipefds[0], -1, clientfd, -1, -1, SPLICE_F_MOVE | SPLICE_F_NONBLOCK); 46 | if (r <= 0) break; 47 | # else 48 | int r = co_await service.splice(clientfd, -1, pipefds[1], -1, -1, SPLICE_F_MOVE); 49 | if (r <= 0) break; 50 | co_await service.splice(pipefds[0], -1, clientfd, -1, r, SPLICE_F_MOVE); 51 | # endif 52 | #else 53 | # if USE_LINK 54 | # error "This won't work because short read of IORING_OP_RECV is not considered an error" 55 | # else 56 | int r = co_await service.recv(clientfd, buf.data(), BUF_SIZE, MSG_NOSIGNAL); 57 | if (r <= 0) break; 58 | co_await service.send(clientfd, buf.data(), r, MSG_NOSIGNAL); 59 | # endif 60 | #endif 61 | } 62 | service.shutdown(clientfd, SHUT_RDWR, IOSQE_IO_LINK); 63 | co_await service.close(clientfd); 64 | fmt::print("sockfd {} is closed; number of running coroutines: {}\n", 65 | clientfd, --runningCoroutines); 66 | }(service, clientfd); 67 | } 68 | } 69 | 70 | int main(int argc, char *argv[]) { 71 | using uio::io_service; 72 | using uio::panic_on_err; 73 | using uio::on_scope_exit; 74 | using uio::panic; 75 | 76 | uint16_t server_port = 0; 77 | if (argc == 2) { 78 | server_port = (uint16_t)std::strtoul(argv[1], nullptr, 10); 79 | } 80 | if (server_port == 0) { 81 | fmt::print("Usage: {} \n", argv[0]); 82 | return 1; 83 | } 84 | 85 | io_service service(MAX_CONN_SIZE); 86 | 87 | int sockfd = socket(AF_INET, SOCK_STREAM, 0) | panic_on_err("socket creation", true); 88 | on_scope_exit closesock([=]() { shutdown(sockfd, SHUT_RDWR); }); 89 | 90 | if (sockaddr_in addr = { 91 | .sin_family = AF_INET, 92 | .sin_port = htons(server_port), 93 | .sin_addr = { INADDR_ANY }, 94 | .sin_zero = {}, 95 | }; bind(sockfd, reinterpret_cast(&addr), sizeof (sockaddr_in))) panic("socket binding", errno); 96 | 97 | if (listen(sockfd, MAX_CONN_SIZE * 2)) panic("listen", errno); 98 | fmt::print("Listening: {}\n", server_port); 99 | 100 | service.run(accept_connection(service, sockfd)); 101 | } 102 | -------------------------------------------------------------------------------- /include/liburing/task.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace uio { 11 | template 12 | struct task; 13 | 14 | // only for internal usage 15 | template 16 | struct task_promise_base { 17 | task get_return_object(); 18 | auto initial_suspend() { return std::suspend_never(); } 19 | auto final_suspend() noexcept { 20 | struct Awaiter: std::suspend_always { 21 | task_promise_base *me_; 22 | 23 | Awaiter(task_promise_base *me): me_(me) {}; 24 | std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) const noexcept { 25 | if (__builtin_expect(me_->result_.index() == 3, false)) { 26 | // FIXME: destroy current coroutine; otherwise memory leaks. 27 | if (me_->waiter_) { 28 | me_->waiter_.destroy(); 29 | } 30 | std::coroutine_handle::from_promise(*me_).destroy(); 31 | } else if (me_->waiter_) { 32 | return me_->waiter_; 33 | } 34 | return std::noop_coroutine(); 35 | } 36 | }; 37 | return Awaiter(this); 38 | } 39 | void unhandled_exception() { 40 | if constexpr (!nothrow) { 41 | if (__builtin_expect(result_.index() == 3, false)) return; 42 | result_.template emplace<2>(std::current_exception()); 43 | } else { 44 | __builtin_unreachable(); 45 | } 46 | } 47 | 48 | protected: 49 | friend struct task; 50 | task_promise_base() = default; 51 | std::coroutine_handle<> waiter_; 52 | std::variant< 53 | std::monostate, 54 | std::conditional_t, std::monostate, T>, 55 | std::conditional_t, 56 | std::monostate // indicates that the promise is detached 57 | > result_; 58 | }; 59 | 60 | // only for internal usage 61 | template 62 | struct task_promise final: task_promise_base { 63 | using task_promise_base::result_; 64 | 65 | template 66 | void return_value(U&& u) { 67 | if (__builtin_expect(result_.index() == 3, false)) return; 68 | result_.template emplace<1>(static_cast(u)); 69 | } 70 | void return_value(int u) { 71 | if (__builtin_expect(result_.index() == 3, false)) return; 72 | result_.template emplace<1>(u); 73 | } 74 | }; 75 | 76 | template 77 | struct task_promise final: task_promise_base { 78 | using task_promise_base::result_; 79 | 80 | void return_void() { 81 | if (__builtin_expect(result_.index() == 3, false)) return; 82 | result_.template emplace<1>(std::monostate {}); 83 | } 84 | }; 85 | 86 | /** 87 | * An awaitable object that returned by an async function 88 | * @tparam T value type holded by this task 89 | * @tparam nothrow if true, the coroutine assigned by this task won't throw exceptions ( slightly better performance ) 90 | * @warning do NOT discard this object when returned by some function, or UB WILL happen 91 | */ 92 | template 93 | struct task final { 94 | using promise_type = task_promise; 95 | using handle_t = std::coroutine_handle; 96 | 97 | task(const task&) = delete; 98 | task& operator =(const task&) = delete; 99 | 100 | bool await_ready() { 101 | auto& result_ = coro_.promise().result_; 102 | return result_.index() > 0; 103 | } 104 | 105 | template 106 | void await_suspend(std::coroutine_handle> caller) noexcept { 107 | coro_.promise().waiter_ = caller; 108 | } 109 | 110 | T await_resume() const { 111 | return get_result(); 112 | } 113 | 114 | /** Get the result hold by this task */ 115 | T get_result() const { 116 | auto& result_ = coro_.promise().result_; 117 | assert(result_.index() != 0); 118 | if constexpr (!nothrow) { 119 | if (auto* pep = std::get_if<2>(&result_)) { 120 | std::rethrow_exception(*pep); 121 | } 122 | } 123 | if constexpr (!std::is_void_v) { 124 | return *std::get_if<1>(&result_); 125 | } 126 | } 127 | 128 | /** Get is the coroutine done */ 129 | bool done() const { 130 | return coro_.done(); 131 | } 132 | 133 | /** Only for placeholder */ 134 | task(): coro_(nullptr) {}; 135 | 136 | task(task&& other) noexcept { 137 | coro_ = std::exchange(other.coro_, nullptr); 138 | } 139 | 140 | task& operator =(task&& other) noexcept { 141 | if (coro_) coro_.destroy(); 142 | coro_ = std::exchange(other.coro_, nullptr); 143 | return *this; 144 | } 145 | 146 | /** Destroy (when done) or detach (when not done) the task object */ 147 | ~task() { 148 | if (!coro_) return; 149 | if (!coro_.done()) { 150 | coro_.promise().result_.template emplace<3>(std::monostate{}); 151 | } else { 152 | coro_.destroy(); 153 | } 154 | } 155 | 156 | private: 157 | friend struct task_promise_base; 158 | task(promise_type *p): coro_(handle_t::from_promise(*p)) {} 159 | handle_t coro_; 160 | }; 161 | 162 | template 163 | task task_promise_base::get_return_object() { 164 | return task(static_cast *>(this)); 165 | } 166 | 167 | } // namespace uio 168 | -------------------------------------------------------------------------------- /functions.cmake: -------------------------------------------------------------------------------- 1 | ################################################ 2 | ## Defaults, Definitions and helper functions ## 3 | ################################################ 4 | 5 | # PROJECT_IS_TOP_LEVEL is a variable added in CMake 3.21 that checks if the 6 | # current project is the top-level project. This checks if it's built in, 7 | # and if not, adds a definition for it. 8 | if("${CMAKE_VERSION}" VERSION_LESS "3.21.0") 9 | if("${CMAKE_PROJECT_NAME}" STREQUAL "${PROJECT_NAME}") 10 | set(PROJECT_IS_TOP_LEVEL ON) 11 | else() 12 | set(PROJECT_IS_TOP_LEVEL OFF) 13 | endif() 14 | endif() 15 | 16 | # Defines some useful constants representing terminal codes to print things 17 | # in color. 18 | if(NOT WIN32) 19 | string(ASCII 27 Esc) 20 | set(ColorReset "${Esc}[m") 21 | set(ColorBold "${Esc}[1m") 22 | set(Red "${Esc}[31m") 23 | set(Green "${Esc}[32m") 24 | set(Yellow "${Esc}[33m") 25 | set(Blue "${Esc}[34m") 26 | set(Magenta "${Esc}[35m") 27 | set(Cyan "${Esc}[36m") 28 | set(White "${Esc}[37m") 29 | set(BoldRed "${Esc}[1;31m") 30 | set(BoldGreen "${Esc}[1;32m") 31 | set(BoldYellow "${Esc}[1;33m") 32 | set(BoldBlue "${Esc}[1;34m") 33 | set(BoldMagenta "${Esc}[1;35m") 34 | set(BoldCyan "${Esc}[1;36m") 35 | set(BoldWhite "${Esc}[1;37m") 36 | endif() 37 | 38 | # Define a function 'note' that prints a message in bold cyan 39 | function(note msg) 40 | message("🐈 ${BoldCyan}says: ${msg}${ColorReset}") 41 | endfunction() 42 | 43 | #################################################### 44 | ## Sec. 2: Dependency Management via FetchContent ## 45 | #################################################### 46 | 47 | set(remote_dependencies "") 48 | 49 | # If ALWAYS_FETCH is ON, then find_or_fetch will always fetch any remote 50 | # dependencies rather than using the ones provided by the system. This is 51 | # useful for creating a static executable. 52 | option( 53 | ALWAYS_FETCH 54 | "Tells find_or_fetch to always fetch packages" 55 | OFF) 56 | 57 | 58 | include(FetchContent) 59 | # find_or_fetch will search for a system installation of ${package} via 60 | # find_package. If it fails to find one, it'll use FetchContent to download and 61 | # build it locally. 62 | function(find_or_fetch package repo tag) 63 | if (NOT ALWAYS_FETCH) 64 | find_package(${package} QUIET) 65 | endif() 66 | 67 | if (ALWAYS_FETCH OR NOT ${${package}_FOUND}) 68 | note("Fetching dependency '${package}' from ${repo}") 69 | include(FetchContent) 70 | FetchContent_Declare( 71 | "${package}" 72 | GIT_REPOSITORY "${repo}" 73 | GIT_TAG "${tag}" 74 | ) 75 | list(APPEND remote_dependencies "${package}") 76 | set (remote_dependencies ${remote_dependencies} PARENT_SCOPE) 77 | else() 78 | note("Using system cmake package for dependency '${package}'") 79 | endif() 80 | endfunction() 81 | 82 | ##################################################################### 83 | ## Sec. 3: Convinience Functions to add targets more automatically ## 84 | ##################################################################### 85 | 86 | 87 | # Adds every top-level .cpp file in the given directory as an executable. Arguments 88 | # provided after the directory name are interpreted as libraries, and it'll link 89 | # targets in that directory against those libraries. 90 | function(add_source_dir dir) 91 | if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}") 92 | file(GLOB all_targets "${dir}/*.cpp") 93 | string(REPLACE ";" ", " library_list "${ARGN}") 94 | foreach(filename ${all_targets}) 95 | get_filename_component(target ${filename} NAME_WLE) 96 | note("Adding '${target}' from ${dir}/${target}.cpp with libraries ${library_list}") 97 | add_executable("${target}" "${filename}") 98 | target_link_libraries("${target}" PRIVATE ${ARGN}) 99 | endforeach() 100 | else() 101 | note("add_source_dir: Skipping ${dir}. Directory not found.") 102 | endif() 103 | endfunction() 104 | 105 | # Adds every top-level .cpp file in the given directory as an executable. Arguments 106 | # provided after the directory name are interpreted as libraries, and it'll link 107 | # targets in that directory against those libraries. Each target will also be 108 | # registered as a test via CTest 109 | function(add_test_dir dir) 110 | if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}") 111 | file(GLOB all_targets "${dir}/*.cpp") 112 | string(REPLACE ";" ", " library_list "${ARGN}") 113 | foreach(filename ${all_targets}) 114 | get_filename_component(target ${filename} NAME_WLE) 115 | # Tests are named test_{name of test} 116 | set(target test_${target}) 117 | note("Adding test '${target}' from ${dir}/${target}.cpp with libraries ${library_list}") 118 | add_executable("${target}" "${filename}") 119 | target_link_libraries("${target}" PRIVATE ${ARGN}) 120 | add_test(NAME "${target}" COMMAND "${target}") 121 | endforeach() 122 | else() 123 | note("add_test_dir: Skipping ${dir}. Directory not found.") 124 | endif() 125 | endfunction() 126 | 127 | 128 | # Targets C++20 for a given target. also adds additional compiler options 129 | # in order to ensure greater levels of compatibility. 130 | function(target_cpp_20 target_name) 131 | target_compile_features(${target_name} INTERFACE cxx_std_20) 132 | 133 | # The /EHa flag enables standard C++ stack unwinding 134 | # See: https://docs.microsoft.com/en-us/cpp/build/reference/eh-exception-handling-model?view=msvc-160 135 | if (MSVC) 136 | target_compile_options(${target_name} INTERFACE "/EHa") 137 | endif() 138 | 139 | if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 140 | # This definition is needed b/c the header needs it in order 141 | # to work on clang 142 | target_compile_definitions(${target_name} INTERFACE __cpp_impl_coroutine=1) 143 | endif() 144 | 145 | # Enables GCC support for coroutines (these are standard C++ now but GCC still 146 | # requires a flag for them) 147 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 148 | target_compile_options(${target_name} INTERFACE "-fcoroutines") 149 | endif() 150 | endfunction() 151 | 152 | 153 | function(add_submodules libname dir) 154 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}") 155 | file(GLOB all_modules "${dir}/*") 156 | foreach(module_dir ${all_modules}) 157 | get_filename_component(module ${module_dir} NAME) 158 | note("Linked ${module} @ ${dir}/${module}") 159 | add_subdirectory("${dir}/${module}") 160 | target_link_libraries(${libname} INTERFACE ${module}) 161 | endforeach() 162 | endif() 163 | endfunction() 164 | -------------------------------------------------------------------------------- /demo/file_server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include // https://github.com/fmtlib/fmt 12 | #include 13 | 14 | #include 15 | 16 | enum { 17 | SERVER_PORT = 8080, 18 | BUF_SIZE = 1024, 19 | }; 20 | 21 | using namespace std::literals; 22 | 23 | // Predefined HTTP error response headers 24 | static constexpr const auto http_400_hdr = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n"sv; 25 | static constexpr const auto http_403_hdr = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n"sv; 26 | static constexpr const auto http_404_hdr = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"sv; 27 | 28 | int runningCoroutines = 0; 29 | 30 | // Serve response 31 | uio::task<> http_send_file(uio::io_service& service, std::string filename, int clientfd, int dirfd) { 32 | using uio::on_scope_exit; 33 | using uio::panic_on_err; 34 | using uio::dur2ts; 35 | 36 | if (filename == "./") filename = "./index.html"; 37 | 38 | const auto infd = co_await service.openat(dirfd, filename.c_str(), O_RDONLY, 0); 39 | if (infd < 0) { 40 | fmt::print("{}: file not found!\n", filename); 41 | co_await service.send(clientfd, http_404_hdr.data(), http_404_hdr.size(), MSG_NOSIGNAL) | uio::panic_on_err("send" , false); 42 | co_return; 43 | } 44 | 45 | on_scope_exit closefd([&]() { service.close(infd); }); 46 | 47 | if (struct stat st; fstat(infd, &st) || !S_ISREG(st.st_mode)) { 48 | fmt::print("{}: not a regular file!\n", filename); 49 | co_await service.send(clientfd, http_403_hdr.data(), http_403_hdr.size(), MSG_NOSIGNAL) | panic_on_err("send" , false); 50 | } else { 51 | auto contentType = [filename_view = std::string_view(filename)]() { 52 | auto extension = filename_view.substr(filename_view.find_last_of('.') + 1); 53 | if (extension == "txt"sv || extension == "c"sv || extension == "cpp"sv || extension == "h"sv || extension == "hpp"sv) { 54 | return "text/plain"sv; 55 | } 56 | return "application/octet-stream"sv; 57 | }(); 58 | 59 | auto header = fmt::format("HTTP/1.1 200 OK\r\nContent-type: {}\r\nContent-Length: {}\r\n\r\n", contentType, st.st_size); 60 | co_await service.send(clientfd, header.data(), header.size(), MSG_NOSIGNAL | MSG_MORE) | panic_on_err("send" , false); 61 | 62 | off_t offset = 0; 63 | std::array filebuf; 64 | for (; st.st_size - offset > BUF_SIZE; offset += BUF_SIZE) { 65 | auto t = service.read(infd, filebuf.data(), filebuf.size(), offset, IOSQE_IO_LINK) | panic_on_err("read" , false); 66 | co_await service.send(clientfd, filebuf.data(), filebuf.size(), MSG_NOSIGNAL | MSG_MORE) | panic_on_err("send", false); 67 | auto ts = dur2ts(100ms); 68 | co_await service.timeout(&ts) | panic_on_err("timeout" , false); // For debugging 69 | } 70 | if (st.st_size > offset) { 71 | auto t = service.read(infd, filebuf.data(), st.st_size - offset, offset, IOSQE_IO_LINK) | panic_on_err("read", false); 72 | co_await service.send(clientfd, filebuf.data(), st.st_size - offset, MSG_NOSIGNAL) | panic_on_err("send", false); 73 | } 74 | } 75 | } 76 | 77 | // Parse HTTP request header 78 | uio::task<> serve(uio::io_service& service, int clientfd, int dirfd) { 79 | using uio::panic_on_err; 80 | 81 | fmt::print("Serving connection, sockfd {}; number of running coroutines: {}\n", 82 | clientfd, runningCoroutines); 83 | 84 | std::array buffer; 85 | 86 | int res = co_await service.recv(clientfd, buffer.data(), buffer.size(), 0) | panic_on_err("recv", false); 87 | 88 | std::string_view buf_view = std::string_view(buffer.data(), size_t(res)); 89 | 90 | // We only handle GET requests, for simplification 91 | if (buf_view.compare(0, 3, "GET") == 0) { 92 | auto file = "."s += buf_view.substr(4, buf_view.find(' ', 4) - 4); 93 | fmt::print("received request {} with sockfd {}\n", file, clientfd); 94 | co_await http_send_file(service, file, clientfd, dirfd); 95 | } else { 96 | fmt::print("unsupported request: {}\n", buf_view); 97 | co_await service.send(clientfd, http_400_hdr.data(), http_400_hdr.size(), MSG_NOSIGNAL) | panic_on_err("send", false); 98 | } 99 | } 100 | 101 | uio::task<> accept_connection(uio::io_service& service, int serverfd, int dirfd) { 102 | using uio::task; 103 | 104 | while (int clientfd = co_await service.accept(serverfd, nullptr, nullptr)) { 105 | // Start worker coroutine to handle new requests 106 | [](uio::io_service& service, int dirfd, int clientfd) -> task<> { 107 | ++runningCoroutines; 108 | auto start = std::chrono::high_resolution_clock::now(); 109 | try { 110 | co_await serve(service, clientfd, dirfd); 111 | } catch (std::exception& e) { 112 | fmt::print("sockfd {} crashed with exception: {}\n", 113 | clientfd, 114 | e.what()); 115 | } 116 | 117 | // Clean up 118 | co_await service.shutdown(clientfd, SHUT_RDWR); 119 | co_await service.close(clientfd); 120 | fmt::print("sockfd {} is closed, time used {:%T}\n", 121 | clientfd, 122 | std::chrono::high_resolution_clock::now() - start); 123 | --runningCoroutines; 124 | }(service, dirfd, clientfd); 125 | } 126 | } 127 | 128 | int main(int argc, char* argv[]) { 129 | using uio::panic_on_err; 130 | using uio::on_scope_exit; 131 | using uio::panic; 132 | using uio::io_service; 133 | 134 | if (argc != 2) { 135 | fmt::print("Usage: {} \n", argv[0]); 136 | return 1; 137 | } 138 | 139 | int dirfd = open(argv[1], O_DIRECTORY) | panic_on_err("open dir", true); 140 | on_scope_exit closedir([=]() { close(dirfd); }); 141 | 142 | int sockfd = socket(AF_INET, SOCK_STREAM, 0) | panic_on_err("socket creation", true); 143 | on_scope_exit closesock([=]() { close(sockfd); }); 144 | 145 | if (int on = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on))) panic("SO_REUSEADDR", errno); 146 | if (int on = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof (on))) panic("SO_REUSEPORT", errno); 147 | 148 | if (sockaddr_in addr = { 149 | .sin_family = AF_INET, 150 | .sin_port = htons(SERVER_PORT), 151 | .sin_addr = { INADDR_ANY }, 152 | .sin_zero = {}, // Silense compiler warnings 153 | }; bind(sockfd, reinterpret_cast(&addr), sizeof (sockaddr_in))) panic("socket binding", errno); 154 | 155 | if (listen(sockfd, 128)) panic("listen", errno); 156 | fmt::print("Listening: {}\n", (uint16_t) SERVER_PORT); 157 | 158 | io_service service; 159 | 160 | // Start main coroutine ( for co_await ) 161 | service.run(accept_connection(service, sockfd, dirfd)); 162 | } 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # liburing4cpp 2 | 3 | Modern C++ binding for [liburing](https://github.com/axboe/liburing) that uses C++20 Coroutines ( but still compiles for `clang` at C++17 mode with `-fcoroutines-ts` ) 4 | 5 | Originally named liburing-http-demo ( this project was originally started for demo ) 6 | 7 | ## Requirements 8 | 9 | Requires the latest kernel ( currently 5.8 ). Since [io_uring](https://git.kernel.dk/cgit/liburing/) is in active development, we will drop old kernel support when every new linux kernel version is released ( before the next LTS version is released, maybe ). 10 | 11 | Tested: `Ubuntu 5.9.0-050900rc6daily20200923-generic #202009222208 SMP Wed Sep 23 02:24:13 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux` with `clang version 10.0.0-4ubuntu1` 12 | 13 | ## First glance 14 | 15 | ```cpp 16 | #include 17 | 18 | using namespace std::literals; 19 | 20 | int main() { 21 | // You first need an io_service instance 22 | uio::io_service service; 23 | 24 | // In order to `co_await`, you must be in a coroutine. 25 | // We use IIFE here for simplification 26 | auto work = [&] () -> uio::task<> { 27 | // Use Linux syscalls just as what you did before (except a little changes) 28 | const auto str = "Hello world\n"sv; 29 | co_await service.write(STDOUT_FILENO, str.data(), str.size(), 0); 30 | }(); 31 | 32 | // At last, you need a loop to dispatch finished IO events 33 | // It's usually called Event Loop (https://en.wikipedia.org/wiki/Event_loop) 34 | service.run(work); 35 | } 36 | ``` 37 | 38 | ## Benchmarks 39 | 40 | * Ubuntu 20.04.1 LTS 41 | * Linux Ubuntu 5.9.0-050900rc6daily20200923-generic #202009222208 SMP Wed Sep 23 02:24:13 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux 42 | * Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz 43 | * Compiler: clang version 10.0.0-4ubuntu1 44 | 45 | ### demo/bench 46 | 47 | ``` 48 | service.yield: 5436209973 49 | plain IORING_OP_NOP: 5268565967 50 | this_thread::yield: 4750992301 51 | pause: 41557653 52 | ``` 53 | 54 | About 3% overhead 55 | 56 | ### demo/echo_server 57 | 58 | * demo/echo_server 12345 ( C++, uses coroutines ) 59 | * [./io_uring_echo_server 12345](https://github.com/CarterLi/io_uring-echo-server) ( C, raw ) 60 | 61 | with `rust_echo_bench`: https://github.com/haraldh/rust_echo_bench 62 | unit: request/sec 63 | 64 | Also see [benchmarks for different opcodes](https://github.com/CarterLi/io_uring-echo-server#benchmarks) 65 | 66 | #### command: `cargo run --release` 67 | 68 | LANG | USE_LINK | USE_SPLICE | USE_POLL | operations | 1st | 2nd | 3rd | mid | rate 69 | :-: | :-: | :-: | :-: | -: | -: | -: | -: | -: | -: 70 | C | - | - | 0 | RECV-SEND | 114461 | 116797 | 112112 | 114461 | 100.00% 71 | C | - | - | 1 | POLL-RECV-SEND | 109037 | 114893 | 117629 | 114893 | 100.38% 72 | C++ | 0 | 0 | 0 | RECV-SEND | 117519 | 121139 | 120239 | 120239 | 105.05% 73 | C++ | 0 | 1 | 0 | SPLICE-SPLICE | 90577 | 91912 | 92301 | 91912 | 80.30% 74 | C++ | 1 | 1 | 0 | SPLICE-SPLICE | 93440 | 92619 | 94201 | 93440 | 81.63% 75 | C++ | 0 | 0 | 1 | POLL-RECV-SEND | 107454 | 111525 | 111210 | 111210 | 97.16% 76 | C++ | 0 | 1 | 1 | POLL-SPLICE-SPLICE | 89469 | 90663 | 89315 | 89469 | 78.17% 77 | C++ | 1 | 1 | 1 | POLL-SPLICE-SPLICE | 87628 | 89099 | 88708 | 89099 | 77.84% 78 | 79 | ## Project Structure 80 | 81 | ### task.hpp 82 | 83 | An awaitable class for C++2a coroutine functions. Originally modified from [gor_task.h](https://github.com/Quuxplusone/coro#taskh-gor_taskh) 84 | 85 | NOTE: `task` is not lazily executed, which is easy to use of course, but also can be easily misused. The simplest code to crash your memory is: 86 | 87 | ```c++ 88 | { 89 | char c; 90 | service.read(STDIN_FILENO, &c, sizeof (c), 0); 91 | } 92 | ``` 93 | 94 | The task instance returned by `service.read` is destructed, but the kernel task itself is **NOT** canceled. The memory of variable `c` will be written sometime. In this case, out-of-scope stack memory access will happen. 95 | 96 | ### io_service.hpp 97 | 98 | Main [liburing](https://github.com/axboe/liburing) binding. Also provides some helper functions for working with posix interfaces easier. 99 | 100 | ### demo 101 | 102 | Some examples 103 | 104 | #### file_server.cpp 105 | 106 | A simple http file server that returns file's content requested by clients 107 | 108 | #### link_cp.cpp 109 | 110 | A cp command inspired by original [liburing link-cp demo](https://github.com/axboe/liburing/blob/master/examples/link-cp.c) 111 | 112 | #### http_client.cpp 113 | 114 | A simple http client that sends `GET` http request 115 | 116 | #### threading.cpp 117 | 118 | A simple `async_invoke` implementation 119 | 120 | #### test.cpp 121 | 122 | Various simple tests 123 | 124 | #### bench.cpp 125 | 126 | Benchmarks 127 | 128 | #### echo_server.cpp 129 | 130 | Echo server, features IOSQE_IO_LINK and IOSQE_FIXED_FILE 131 | 132 | See also https://github.com/frevib/io_uring-echo-server#benchmarks for benchmarking 133 | 134 | ## Build 135 | 136 | This library is header only. It provides some demos, as well as some tests. 137 | 138 | ### Dependencies 139 | 140 | This library has to be linked against [`liburing`](https://github.com/axboe/liburing), 141 | and requires a recent version of GCC or Clang. For best results, please use GCC 142 | 10.3 (or later), or Clang 10.0.0 (or later) 143 | 144 | **[Optional]** This library can be built with either `libc++` or `libstdc++`. 145 | If you want to use `libc++`, you can install it with 146 | ``` 147 | sudo apt install clang libc++-dev libc++abi-dev` 148 | ``` 149 | 150 | ### Building Demos & Tests 151 | 152 | In order to build the demos, clone the repository and then run CMake in the 153 | project's root directory: 154 | ```bash 155 | git clone https://github.com/CarterLi/liburing4cpp.git 156 | cd liburing4cpp 157 | cmake -B build -DCMAKE_BUILD_TYPE=Release 158 | cmake --build build --config Release 159 | ``` 160 | Optionally, you may also use CMake Presets instead (requires CMake version 3.19 or above) 161 | ```bash 162 | git clone https://github.com/CarterLi/liburing4cpp.git 163 | cd liburing4cpp 164 | cmake --preset=Release 165 | cmake --build --preset=Release 166 | ``` 167 | 168 | Binares are placed in the `build/` directory. You can then run tests via ctest: 169 | ```bash 170 | ctest --test-dir build 171 | ``` 172 | 173 | ## Using this library with CMake 174 | When using CMake, you can automatically include this library as a dependency of 175 | your project by using the FetchContent interface: 176 | 177 | ```cmake 178 | include(FetchContent) 179 | FetchContent_Declare( 180 | liburing4cpp 181 | GIT_REPOSITORY https://github.com/CarterLi/liburing4cpp.git 182 | GIT_TAG async 183 | ) 184 | FetchContent_MakeAvailable(liburing4cpp) 185 | ``` 186 | Then, just use `target_link_libraries`, which will ensure that liburing4cpp/include 187 | is added to the list of includes for whatever target you're building. 188 | ```cmake 189 | target_link_libraries( 190 | 191 | 192 | liburing4cpp) 193 | ``` 194 | 195 | ## License 196 | 197 | MIT 198 | -------------------------------------------------------------------------------- /include/liburing/io_service.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include // http://git.kernel.dk/liburing 11 | #ifndef NDEBUG 12 | # include 13 | #endif 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #ifdef LIBURING_VERBOSE 20 | # define puts_if_verbose(x) puts(x) 21 | # define printf_if_verbose(...) printf(__VA_ARGS__) 22 | #else 23 | # define puts_if_verbose(x) 0 24 | # define printf_if_verbose(...) 0 25 | #endif 26 | 27 | namespace uio { 28 | class io_service { 29 | public: 30 | /** Init io_service / io_uring object 31 | * @see io_uring_setup(2) 32 | * @param entries Maximum sqe can be gotten without submitting 33 | * @param flags flags used to init io_uring 34 | * @param wq_fd existing io_uring ring_fd used by IORING_SETUP_ATTACH_WQ 35 | * @note io_service is NOT thread safe, nor is liburing. When used in a 36 | * multi-threaded program, it's highly recommended to create 37 | * io_service/io_uring instance per thread, and set IORING_SETUP_ATTACH_WQ 38 | * flag to make sure that kernel shares the only async worker thread pool. 39 | * See `IORING_SETUP_ATTACH_WQ` for detail. 40 | */ 41 | io_service(int entries = 64, uint32_t flags = 0, uint32_t wq_fd = 0) { 42 | io_uring_params p = { 43 | .flags = flags, 44 | .wq_fd = wq_fd, 45 | }; 46 | 47 | io_uring_queue_init_params(entries, &ring, &p) | panic_on_err("queue_init_params", false); 48 | 49 | auto* probe = io_uring_get_probe_ring(&ring); 50 | on_scope_exit free_probe([=]() { io_uring_free_probe(probe); }); 51 | #define TEST_IORING_OP(opcode) do {\ 52 | for (int i = 0; i < probe->ops_len; ++i) {\ 53 | if (probe->ops[i].op == opcode && probe->ops[i].flags & IO_URING_OP_SUPPORTED) {\ 54 | probe_ops[i] = true;\ 55 | puts_if_verbose("\t" #opcode);\ 56 | break;\ 57 | }\ 58 | }\ 59 | } while (0) 60 | puts_if_verbose("Supported io_uring opcodes by current kernel:"); 61 | TEST_IORING_OP(IORING_OP_NOP); 62 | TEST_IORING_OP(IORING_OP_READV); 63 | TEST_IORING_OP(IORING_OP_WRITEV); 64 | TEST_IORING_OP(IORING_OP_FSYNC); 65 | TEST_IORING_OP(IORING_OP_READ_FIXED); 66 | TEST_IORING_OP(IORING_OP_WRITE_FIXED); 67 | TEST_IORING_OP(IORING_OP_POLL_ADD); 68 | TEST_IORING_OP(IORING_OP_POLL_REMOVE); 69 | TEST_IORING_OP(IORING_OP_SYNC_FILE_RANGE); 70 | TEST_IORING_OP(IORING_OP_SENDMSG); 71 | TEST_IORING_OP(IORING_OP_RECVMSG); 72 | TEST_IORING_OP(IORING_OP_TIMEOUT); 73 | TEST_IORING_OP(IORING_OP_TIMEOUT_REMOVE); 74 | TEST_IORING_OP(IORING_OP_ACCEPT); 75 | TEST_IORING_OP(IORING_OP_ASYNC_CANCEL); 76 | TEST_IORING_OP(IORING_OP_LINK_TIMEOUT); 77 | TEST_IORING_OP(IORING_OP_CONNECT); 78 | TEST_IORING_OP(IORING_OP_FALLOCATE); 79 | TEST_IORING_OP(IORING_OP_OPENAT); 80 | TEST_IORING_OP(IORING_OP_CLOSE); 81 | TEST_IORING_OP(IORING_OP_FILES_UPDATE); 82 | TEST_IORING_OP(IORING_OP_STATX); 83 | TEST_IORING_OP(IORING_OP_READ); 84 | TEST_IORING_OP(IORING_OP_WRITE); 85 | TEST_IORING_OP(IORING_OP_FADVISE); 86 | TEST_IORING_OP(IORING_OP_MADVISE); 87 | TEST_IORING_OP(IORING_OP_SEND); 88 | TEST_IORING_OP(IORING_OP_RECV); 89 | TEST_IORING_OP(IORING_OP_OPENAT2); 90 | TEST_IORING_OP(IORING_OP_EPOLL_CTL); 91 | TEST_IORING_OP(IORING_OP_SPLICE); 92 | TEST_IORING_OP(IORING_OP_PROVIDE_BUFFERS); 93 | TEST_IORING_OP(IORING_OP_REMOVE_BUFFERS); 94 | TEST_IORING_OP(IORING_OP_TEE); 95 | TEST_IORING_OP(IORING_OP_SHUTDOWN); 96 | TEST_IORING_OP(IORING_OP_RENAMEAT); 97 | TEST_IORING_OP(IORING_OP_UNLINKAT); 98 | TEST_IORING_OP(IORING_OP_MKDIRAT); 99 | TEST_IORING_OP(IORING_OP_SYMLINKAT); 100 | TEST_IORING_OP(IORING_OP_LINKAT); 101 | TEST_IORING_OP(IORING_OP_MSG_RING); 102 | TEST_IORING_OP(IORING_OP_FSETXATTR); 103 | TEST_IORING_OP(IORING_OP_SETXATTR); 104 | TEST_IORING_OP(IORING_OP_FGETXATTR); 105 | TEST_IORING_OP(IORING_OP_GETXATTR); 106 | TEST_IORING_OP(IORING_OP_SOCKET); 107 | TEST_IORING_OP(IORING_OP_URING_CMD); 108 | TEST_IORING_OP(IORING_OP_SEND_ZC); 109 | TEST_IORING_OP(IORING_OP_SENDMSG_ZC); 110 | #undef TEST_IORING_OP 111 | 112 | #define TEST_IORING_FEATURE(feature) if (p.features & feature) puts_if_verbose("\t" #feature) 113 | puts_if_verbose("Supported io_uring features by current kernel:"); 114 | TEST_IORING_FEATURE(IORING_FEAT_SINGLE_MMAP); 115 | TEST_IORING_FEATURE(IORING_FEAT_NODROP); 116 | TEST_IORING_FEATURE(IORING_FEAT_SUBMIT_STABLE); 117 | TEST_IORING_FEATURE(IORING_FEAT_RW_CUR_POS); 118 | TEST_IORING_FEATURE(IORING_FEAT_CUR_PERSONALITY); 119 | TEST_IORING_FEATURE(IORING_FEAT_FAST_POLL); 120 | TEST_IORING_FEATURE(IORING_FEAT_POLL_32BITS); 121 | TEST_IORING_FEATURE(IORING_FEAT_SQPOLL_NONFIXED); 122 | TEST_IORING_FEATURE(IORING_FEAT_EXT_ARG); 123 | TEST_IORING_FEATURE(IORING_FEAT_NATIVE_WORKERS); 124 | TEST_IORING_FEATURE(IORING_FEAT_RSRC_TAGS); 125 | TEST_IORING_FEATURE(IORING_FEAT_CQE_SKIP); 126 | TEST_IORING_FEATURE(IORING_FEAT_LINKED_FILE); 127 | TEST_IORING_FEATURE(IORING_FEAT_REG_REG_RING); 128 | #undef TEST_IORING_FEATURE 129 | } 130 | 131 | /** Destroy io_service / io_uring object */ 132 | ~io_service() noexcept { 133 | io_uring_queue_exit(&ring); 134 | } 135 | 136 | // io_service is not copyable. It can be moveable but humm... 137 | io_service(const io_service&) = delete; 138 | io_service& operator =(const io_service&) = delete; 139 | 140 | public: 141 | 142 | /** Read data into multiple buffers asynchronously 143 | * @see preadv2(2) 144 | * @see io_uring_enter(2) IORING_OP_READV 145 | * @param iflags IOSQE_* flags 146 | * @return a task object for awaiting 147 | */ 148 | sqe_awaitable readv( 149 | int fd, 150 | const iovec* iovecs, 151 | unsigned nr_vecs, 152 | off_t offset, 153 | uint8_t iflags = 0 154 | ) noexcept { 155 | auto* sqe = io_uring_get_sqe_safe(); 156 | io_uring_prep_readv(sqe, fd, iovecs, nr_vecs, offset); 157 | return await_work(sqe, iflags); 158 | } 159 | 160 | sqe_awaitable readv2( 161 | int fd, 162 | const iovec* iovecs, 163 | unsigned nr_vecs, 164 | off_t offset, 165 | int flags, 166 | uint8_t iflags = 0 167 | ) noexcept { 168 | auto* sqe = io_uring_get_sqe_safe(); 169 | io_uring_prep_readv2(sqe, fd, iovecs, nr_vecs, offset, flags); 170 | return await_work(sqe, iflags); 171 | } 172 | 173 | /** Write data into multiple buffers asynchronously 174 | * @see pwritev2(2) 175 | * @see io_uring_enter(2) IORING_OP_WRITEV 176 | * @param iflags IOSQE_* flags 177 | * @return a task object for awaiting 178 | */ 179 | sqe_awaitable writev( 180 | int fd, 181 | const iovec* iovecs, 182 | unsigned nr_vecs, 183 | off_t offset, 184 | uint8_t iflags = 0 185 | ) noexcept { 186 | auto* sqe = io_uring_get_sqe_safe(); 187 | io_uring_prep_writev(sqe, fd, iovecs, nr_vecs, offset); 188 | return await_work(sqe, iflags); 189 | } 190 | 191 | sqe_awaitable writev2( 192 | int fd, 193 | const iovec* iovecs, 194 | unsigned nr_vecs, 195 | off_t offset, 196 | int flags, 197 | uint8_t iflags = 0 198 | ) noexcept { 199 | auto* sqe = io_uring_get_sqe_safe(); 200 | io_uring_prep_writev2(sqe, fd, iovecs, nr_vecs, offset, flags); 201 | return await_work(sqe, iflags); 202 | } 203 | 204 | /** Read from a file descriptor at a given offset asynchronously 205 | * @see pread(2) 206 | * @see io_uring_enter(2) IORING_OP_READ 207 | * @param iflags IOSQE_* flags 208 | * @return a task object for awaiting 209 | */ 210 | sqe_awaitable read( 211 | int fd, 212 | void* buf, 213 | unsigned nbytes, 214 | off_t offset, 215 | uint8_t iflags = 0 216 | ) { 217 | auto* sqe = io_uring_get_sqe_safe(); 218 | io_uring_prep_read(sqe, fd, buf, nbytes, offset); 219 | return await_work(sqe, iflags); 220 | } 221 | 222 | /** Write to a file descriptor at a given offset asynchronously 223 | * @see pwrite(2) 224 | * @see io_uring_enter(2) IORING_OP_WRITE 225 | * @param iflags IOSQE_* flags 226 | * @return a task object for awaiting 227 | */ 228 | sqe_awaitable write( 229 | int fd, 230 | const void* buf, 231 | unsigned nbytes, 232 | off_t offset, 233 | uint8_t iflags = 0 234 | ) { 235 | auto* sqe = io_uring_get_sqe_safe(); 236 | io_uring_prep_write(sqe, fd, buf, nbytes, offset); 237 | return await_work(sqe, iflags); 238 | } 239 | 240 | /** Read data into a fixed buffer asynchronously 241 | * @see preadv2(2) 242 | * @see io_uring_enter(2) IORING_OP_READ_FIXED 243 | * @param buf_index the index of buffer registered with register_buffers 244 | * @param iflags IOSQE_* flags 245 | * @return a task object for awaiting 246 | */ 247 | sqe_awaitable read_fixed( 248 | int fd, 249 | void* buf, 250 | unsigned nbytes, 251 | off_t offset, 252 | int buf_index, 253 | uint8_t iflags = 0 254 | ) noexcept { 255 | auto* sqe = io_uring_get_sqe_safe(); 256 | io_uring_prep_read_fixed(sqe, fd, buf, nbytes, offset, buf_index); 257 | return await_work(sqe, iflags); 258 | } 259 | 260 | /** Write data into a fixed buffer asynchronously 261 | * @see pwritev2(2) 262 | * @see io_uring_enter(2) IORING_OP_WRITE_FIXED 263 | * @param buf_index the index of buffer registered with register_buffers 264 | * @param iflags IOSQE_* flags 265 | * @return a task object for awaiting 266 | */ 267 | sqe_awaitable write_fixed( 268 | int fd, 269 | const void* buf, 270 | unsigned nbytes, 271 | off_t offset, 272 | int buf_index, 273 | uint8_t iflags = 0 274 | ) noexcept { 275 | auto* sqe = io_uring_get_sqe_safe(); 276 | io_uring_prep_write_fixed(sqe, fd, buf, nbytes, offset, buf_index); 277 | return await_work(sqe, iflags); 278 | } 279 | 280 | /** Synchronize a file's in-core state with storage device asynchronously 281 | * @see fsync(2) 282 | * @see io_uring_enter(2) IORING_OP_FSYNC 283 | * @param iflags IOSQE_* flags 284 | * @return a task object for awaiting 285 | */ 286 | sqe_awaitable fsync( 287 | int fd, 288 | unsigned fsync_flags, 289 | uint8_t iflags = 0 290 | ) noexcept { 291 | auto* sqe = io_uring_get_sqe_safe(); 292 | io_uring_prep_fsync(sqe, fd, fsync_flags); 293 | return await_work(sqe, iflags); 294 | } 295 | 296 | /** Sync a file segment with disk asynchronously 297 | * @see sync_file_range(2) 298 | * @see io_uring_enter(2) IORING_OP_SYNC_FILE_RANGE 299 | * @param iflags IOSQE_* flags 300 | * @return a task object for awaiting 301 | */ 302 | sqe_awaitable sync_file_range( 303 | int fd, 304 | off64_t offset, 305 | off64_t nbytes, 306 | unsigned sync_range_flags, 307 | uint8_t iflags = 0 308 | ) noexcept { 309 | auto* sqe = io_uring_get_sqe_safe(); 310 | io_uring_prep_rw(IORING_OP_SYNC_FILE_RANGE, sqe, fd, nullptr, nbytes, offset); 311 | sqe->sync_range_flags = sync_range_flags; 312 | return await_work(sqe, iflags); 313 | } 314 | 315 | /** Receive a message from a socket asynchronously 316 | * @see recvmsg(2) 317 | * @see io_uring_enter(2) IORING_OP_RECVMSG 318 | * @param iflags IOSQE_* flags 319 | * @return a task object for awaiting 320 | */ 321 | 322 | sqe_awaitable recvmsg( 323 | int sockfd, 324 | msghdr* msg, 325 | uint32_t flags, 326 | uint8_t iflags = 0 327 | ) noexcept { 328 | auto* sqe = io_uring_get_sqe_safe(); 329 | io_uring_prep_recvmsg(sqe, sockfd, msg, flags); 330 | return await_work(sqe, iflags); 331 | } 332 | 333 | /** Send a message on a socket asynchronously 334 | * @see sendmsg(2) 335 | * @see io_uring_enter(2) IORING_OP_SENDMSG 336 | * @param iflags IOSQE_* flags 337 | * @return a task object for awaiting 338 | */ 339 | sqe_awaitable sendmsg( 340 | int sockfd, 341 | const msghdr* msg, 342 | uint32_t flags, 343 | uint8_t iflags = 0 344 | ) noexcept { 345 | auto* sqe = io_uring_get_sqe_safe(); 346 | io_uring_prep_sendmsg(sqe, sockfd, msg, flags); 347 | return await_work(sqe, iflags); 348 | } 349 | 350 | /** Receive a message from a socket asynchronously 351 | * @see recv(2) 352 | * @see io_uring_enter(2) IORING_OP_RECV 353 | * @param iflags IOSQE_* flags 354 | * @return a task object for awaiting 355 | */ 356 | sqe_awaitable recv( 357 | int sockfd, 358 | void* buf, 359 | unsigned nbytes, 360 | uint32_t flags, 361 | uint8_t iflags = 0 362 | ) noexcept { 363 | auto* sqe = io_uring_get_sqe_safe(); 364 | io_uring_prep_recv(sqe, sockfd, buf, nbytes, flags); 365 | return await_work(sqe, iflags); 366 | } 367 | 368 | /** Send a message on a socket asynchronously 369 | * @see send(2) 370 | * @see io_uring_enter(2) IORING_OP_SEND 371 | * @param iflags IOSQE_* flags 372 | * @return a task object for awaiting 373 | */ 374 | sqe_awaitable send( 375 | int sockfd, 376 | const void* buf, 377 | unsigned nbytes, 378 | uint32_t flags, 379 | uint8_t iflags = 0 380 | ) noexcept { 381 | auto* sqe = io_uring_get_sqe_safe(); 382 | io_uring_prep_send(sqe, sockfd, buf, nbytes, flags); 383 | return await_work(sqe, iflags); 384 | } 385 | 386 | /** Wait for an event on a file descriptor asynchronously 387 | * @see poll(2) 388 | * @see io_uring_enter(2) 389 | * @param iflags IOSQE_* flags 390 | * @return a task object for awaiting 391 | */ 392 | sqe_awaitable poll( 393 | int fd, 394 | short poll_mask, 395 | uint8_t iflags = 0 396 | ) noexcept { 397 | auto* sqe = io_uring_get_sqe_safe(); 398 | io_uring_prep_poll_add(sqe, fd, poll_mask); 399 | return await_work(sqe, iflags); 400 | } 401 | 402 | /** Enqueue a NOOP command, which eventually acts like pthread_yield when awaiting 403 | * @see io_uring_enter(2) IORING_OP_NOP 404 | * @param iflags IOSQE_* flags 405 | * @return a task object for awaiting 406 | */ 407 | sqe_awaitable yield( 408 | uint8_t iflags = 0 409 | ) noexcept { 410 | auto* sqe = io_uring_get_sqe_safe(); 411 | io_uring_prep_nop(sqe); 412 | return await_work(sqe, iflags); 413 | } 414 | 415 | /** Accept a connection on a socket asynchronously 416 | * @see accept4(2) 417 | * @see io_uring_enter(2) IORING_OP_ACCEPT 418 | * @param iflags IOSQE_* flags 419 | * @return a task object for awaiting 420 | */ 421 | sqe_awaitable accept( 422 | int fd, 423 | sockaddr *addr, 424 | socklen_t *addrlen, 425 | int flags = 0, 426 | uint8_t iflags = 0 427 | ) noexcept { 428 | auto* sqe = io_uring_get_sqe_safe(); 429 | io_uring_prep_accept(sqe, fd, addr, addrlen, flags); 430 | return await_work(sqe, iflags); 431 | } 432 | 433 | /** Initiate a connection on a socket asynchronously 434 | * @see connect(2) 435 | * @see io_uring_enter(2) IORING_OP_CONNECT 436 | * @param iflags IOSQE_* flags 437 | * @return a task object for awaiting 438 | */ 439 | sqe_awaitable connect( 440 | int fd, 441 | sockaddr *addr, 442 | socklen_t addrlen, 443 | int flags = 0, 444 | uint8_t iflags = 0 445 | ) noexcept { 446 | auto* sqe = io_uring_get_sqe_safe(); 447 | io_uring_prep_connect(sqe, fd, addr, addrlen); 448 | return await_work(sqe, iflags); 449 | } 450 | 451 | /** Wait for specified duration asynchronously 452 | * @see io_uring_enter(2) IORING_OP_TIMEOUT 453 | * @param ts initial expiration, timespec 454 | * @param iflags IOSQE_* flags 455 | * @return a task object for awaiting 456 | */ 457 | sqe_awaitable timeout( 458 | __kernel_timespec *ts, 459 | uint8_t iflags = 0 460 | ) noexcept { 461 | auto* sqe = io_uring_get_sqe_safe(); 462 | io_uring_prep_timeout(sqe, ts, 0, 0); 463 | return await_work(sqe, iflags); 464 | } 465 | 466 | /** Open and possibly create a file asynchronously 467 | * @see openat(2) 468 | * @see io_uring_enter(2) IORING_OP_OPENAT 469 | * @param iflags IOSQE_* flags 470 | * @return a task object for awaiting 471 | */ 472 | sqe_awaitable openat( 473 | int dfd, 474 | const char *path, 475 | int flags, 476 | mode_t mode, 477 | uint8_t iflags = 0 478 | ) noexcept { 479 | auto* sqe = io_uring_get_sqe_safe(); 480 | io_uring_prep_openat(sqe, dfd, path, flags, mode); 481 | return await_work(sqe, iflags); 482 | } 483 | 484 | /** Close a file descriptor asynchronously 485 | * @see close(2) 486 | * @see io_uring_enter(2) IORING_OP_CLOSE 487 | * @param iflags IOSQE_* flags 488 | * @return a task object for awaiting 489 | */ 490 | sqe_awaitable close( 491 | int fd, 492 | uint8_t iflags = 0 493 | ) noexcept { 494 | auto* sqe = io_uring_get_sqe_safe(); 495 | io_uring_prep_close(sqe, fd); 496 | return await_work(sqe, iflags); 497 | } 498 | 499 | /** Get file status asynchronously 500 | * @see statx(2) 501 | * @see io_uring_enter(2) IORING_OP_STATX 502 | * @param iflags IOSQE_* flags 503 | * @return a task object for awaiting 504 | */ 505 | sqe_awaitable statx( 506 | int dfd, 507 | const char *path, 508 | int flags, 509 | unsigned mask, 510 | struct statx *statxbuf, 511 | uint8_t iflags = 0 512 | ) noexcept { 513 | auto* sqe = io_uring_get_sqe_safe(); 514 | io_uring_prep_statx(sqe, dfd, path, flags, mask, statxbuf); 515 | return await_work(sqe, iflags); 516 | } 517 | 518 | /** Splice data to/from a pipe asynchronously 519 | * @see splice(2) 520 | * @see io_uring_enter(2) IORING_OP_SPLICE 521 | * @param iflags IOSQE_* flags 522 | * @return a task object for awaiting 523 | */ 524 | sqe_awaitable splice( 525 | int fd_in, 526 | loff_t off_in, 527 | int fd_out, 528 | loff_t off_out, 529 | size_t nbytes, 530 | unsigned flags, 531 | uint8_t iflags = 0 532 | ) { 533 | auto* sqe = io_uring_get_sqe_safe(); 534 | io_uring_prep_splice(sqe, fd_in, off_in, fd_out, off_out, nbytes, flags); 535 | return await_work(sqe, iflags); 536 | } 537 | 538 | /** Duplicate pipe content asynchronously 539 | * @see tee(2) 540 | * @see io_uring_enter(2) IORING_OP_TEE 541 | * @param iflags IOSQE_* flags 542 | * @return a task object for awaiting 543 | */ 544 | sqe_awaitable tee( 545 | int fd_in, 546 | int fd_out, 547 | size_t nbytes, 548 | unsigned flags, 549 | uint8_t iflags = 0 550 | ) { 551 | auto* sqe = io_uring_get_sqe_safe(); 552 | io_uring_prep_tee(sqe, fd_in, fd_out, nbytes, flags); 553 | return await_work(sqe, iflags); 554 | } 555 | 556 | /** Shut down part of a full-duplex connection asynchronously 557 | * @see shutdown(2) 558 | * @see io_uring_enter(2) IORING_OP_SHUTDOWN 559 | * @param iflags IOSQE_* flags 560 | * @return a task object for awaiting 561 | */ 562 | sqe_awaitable shutdown( 563 | int fd, 564 | int how, 565 | uint8_t iflags = 0 566 | ) { 567 | auto* sqe = io_uring_get_sqe_safe(); 568 | io_uring_prep_shutdown(sqe, fd, how); 569 | return await_work(sqe, iflags); 570 | } 571 | 572 | /** Change the name or location of a file asynchronously 573 | * @see renameat2(2) 574 | * @see io_uring_enter(2) IORING_OP_RENAMEAT 575 | * @param iflags IOSQE_* flags 576 | * @return a task object for awaiting 577 | */ 578 | sqe_awaitable renameat( 579 | int olddfd, 580 | const char *oldpath, 581 | int newdfd, 582 | const char *newpath, 583 | unsigned flags, 584 | uint8_t iflags = 0 585 | ) { 586 | auto* sqe = io_uring_get_sqe_safe(); 587 | io_uring_prep_renameat(sqe, olddfd, oldpath, newdfd, newpath, flags); 588 | return await_work(sqe, iflags); 589 | } 590 | 591 | /** Create a directory asynchronously 592 | * @see mkdirat(2) 593 | * @see io_uring_enter(2) IORING_OP_MKDIRAT 594 | * @param iflags IOSQE_* flags 595 | * @return a task object for awaiting 596 | */ 597 | sqe_awaitable mkdirat( 598 | int dirfd, 599 | const char *pathname, 600 | mode_t mode, 601 | uint8_t iflags = 0 602 | ) { 603 | auto* sqe = io_uring_get_sqe_safe(); 604 | io_uring_prep_mkdirat(sqe, dirfd, pathname, mode); 605 | return await_work(sqe, iflags); 606 | } 607 | 608 | /** Make a new name for a file asynchronously 609 | * @see symlinkat(2) 610 | * @see io_uring_enter(2) IORING_OP_SYMLINKAT 611 | * @param iflags IOSQE_* flags 612 | * @return a task object for awaiting 613 | */ 614 | sqe_awaitable symlinkat( 615 | const char *target, 616 | int newdirfd, 617 | const char *linkpath, 618 | uint8_t iflags = 0 619 | ) { 620 | auto* sqe = io_uring_get_sqe_safe(); 621 | io_uring_prep_symlinkat(sqe, target, newdirfd, linkpath); 622 | return await_work(sqe, iflags); 623 | } 624 | 625 | /** Make a new name for a file asynchronously 626 | * @see linkat(2) 627 | * @see io_uring_enter(2) IORING_OP_LINKAT 628 | * @param iflags IOSQE_* flags 629 | * @return a task object for awaiting 630 | */ 631 | sqe_awaitable linkat( 632 | int olddirfd, 633 | const char *oldpath, 634 | int newdirfd, 635 | const char *newpath, 636 | int flags, 637 | uint8_t iflags = 0 638 | ) { 639 | auto* sqe = io_uring_get_sqe_safe(); 640 | io_uring_prep_linkat(sqe, olddirfd, oldpath, newdirfd, newpath, flags); 641 | return await_work(sqe, iflags); 642 | } 643 | 644 | /** Delete a name and possibly the file it refers to asynchronously 645 | * @see unlinkat(2) 646 | * @see io_uring_enter(2) IORING_OP_UNLINKAT 647 | * @param iflags IOSQE_* flags 648 | * @return a task object for awaiting 649 | */ 650 | sqe_awaitable unlinkat( 651 | int dfd, 652 | const char *path, 653 | unsigned flags, 654 | uint8_t iflags = 0 655 | ) { 656 | auto* sqe = io_uring_get_sqe_safe(); 657 | io_uring_prep_unlinkat(sqe, dfd, path, flags); 658 | return await_work(sqe, iflags); 659 | } 660 | 661 | /** Delete a name and possibly the file it refers to asynchronously 662 | * @see io_uring_enter(2) IORING_OP_MSG_RING 663 | * @param iflags IOSQE_* flags 664 | * @return a task object for awaiting 665 | */ 666 | sqe_awaitable msg_ring( 667 | int fd, 668 | unsigned len, 669 | uint64_t data, 670 | unsigned flags, 671 | uint8_t iflags = 0 672 | ) { 673 | auto* sqe = io_uring_get_sqe_safe(); 674 | io_uring_prep_msg_ring(sqe, fd, len, data, flags); 675 | return await_work(sqe, iflags); 676 | } 677 | 678 | private: 679 | sqe_awaitable await_work( 680 | io_uring_sqe* sqe, 681 | uint8_t iflags 682 | ) noexcept { 683 | io_uring_sqe_set_flags(sqe, iflags); 684 | return sqe_awaitable(sqe); 685 | } 686 | 687 | public: 688 | /** Get a sqe pointer that can never be NULL 689 | * @param ring pointer to inited io_uring struct 690 | * @return pointer to `io_uring_sqe` struct (not NULL) 691 | */ 692 | [[nodiscard]] 693 | io_uring_sqe* io_uring_get_sqe_safe() noexcept { 694 | auto* sqe = io_uring_get_sqe(&ring); 695 | if (__builtin_expect(!!sqe, true)) { 696 | return sqe; 697 | } else { 698 | printf_if_verbose(__FILE__ ": SQ is full, flushing %u cqe(s)\n", cqe_count); 699 | io_uring_cq_advance(&ring, cqe_count); 700 | cqe_count = 0; 701 | io_uring_submit(&ring); 702 | sqe = io_uring_get_sqe(&ring); 703 | if (__builtin_expect(!!sqe, true)) return sqe; 704 | panic("io_uring_get_sqe", ENOMEM); 705 | } 706 | } 707 | 708 | /** Wait for an event forever, blocking 709 | * @see io_uring_wait_cqe 710 | * @see io_uring_enter(2) 711 | * @return a pair of promise pointer (used for resuming suspended coroutine) and retcode of finished command 712 | */ 713 | template 714 | T run(const task& t) noexcept(nothrow) { 715 | while (!t.done()) { 716 | io_uring_submit_and_wait(&ring, 1); 717 | 718 | io_uring_cqe *cqe; 719 | unsigned head; 720 | 721 | io_uring_for_each_cqe(&ring, head, cqe) { 722 | ++cqe_count; 723 | auto coro = static_cast(io_uring_cqe_get_data(cqe)); 724 | if (coro) coro->resolve(cqe->res); 725 | } 726 | 727 | printf_if_verbose(__FILE__ ": Found %u cqe(s), looping...\n", cqe_count); 728 | 729 | io_uring_cq_advance(&ring, cqe_count); 730 | cqe_count = 0; 731 | } 732 | 733 | return t.get_result(); 734 | } 735 | 736 | public: 737 | /** Register files for I/O 738 | * @param fds fds to register 739 | * @see io_uring_register(2) IORING_REGISTER_FILES 740 | */ 741 | void register_files(std::initializer_list fds) { 742 | register_files(fds.begin(), (unsigned int)fds.size()); 743 | } 744 | void register_files(const int *files, unsigned int nr_files) { 745 | io_uring_register_files(&ring, files, nr_files) | panic_on_err("io_uring_register_files", false); 746 | } 747 | 748 | /** Update registered files 749 | * @see io_uring_register(2) IORING_REGISTER_FILES_UPDATE 750 | */ 751 | void register_files_update(unsigned off, int *files, unsigned nr_files) { 752 | io_uring_register_files_update(&ring, off, files, nr_files) | panic_on_err("io_uring_register_files", false); 753 | } 754 | 755 | /** Unregister all files 756 | * @see io_uring_register(2) IORING_UNREGISTER_FILES 757 | */ 758 | int unregister_files() noexcept { 759 | return io_uring_unregister_files(&ring); 760 | } 761 | 762 | public: 763 | /** Register buffers for I/O 764 | * @param ioves array of iovec to register 765 | * @see io_uring_register(2) IORING_REGISTER_BUFFERS 766 | */ 767 | template 768 | void register_buffers(iovec (&&ioves) [N]) { 769 | register_buffers(&ioves[0], N); 770 | } 771 | void register_buffers(const struct iovec *iovecs, unsigned nr_iovecs) { 772 | io_uring_register_buffers(&ring, iovecs, nr_iovecs) | panic_on_err("io_uring_register_buffers", false); 773 | } 774 | 775 | /** Unregister all buffers 776 | * @see io_uring_register(2) IORING_UNREGISTER_BUFFERS 777 | */ 778 | int unregister_buffers() noexcept { 779 | return io_uring_unregister_buffers(&ring); 780 | } 781 | 782 | public: 783 | /** Return internal io_uring handle */ 784 | [[nodiscard]] 785 | io_uring& get_handle() noexcept { 786 | return ring; 787 | } 788 | 789 | private: 790 | io_uring ring; 791 | unsigned cqe_count = 0; 792 | bool probe_ops[IORING_OP_LAST] = {}; 793 | }; 794 | 795 | } // namespace uio 796 | --------------------------------------------------------------------------------