C++23 coroutine network framework
Explore the docs »
View Demo
·
Report Bug
·
Request Feature
Table of Contents
Based on the libuv event loop, use C++20 stackless coroutines to implement network components, and provide channel to send and receive data between tasks.
asyncio might be better than existing coroutine network libraries in the following ways:
- Combination of error codes and exceptions with a comprehensive error handling mechanism.
- Simple and direct task cancellation similar to Python's
asyncio—task.cancel(). - Multiple sub-task aggregation methods with structured concurrency model, inspired by JavaScript's
Promise. - Flexible dynamic task management solution, similar to Golang's
WaitGroup. - Built-in call stack tracing that can traverse the task tree top-down or complete backtracing bottom-up.
Required compiler:
- GCC >= 15
- LLVM >= 18
- MSVC >= 19.38
Export environment variables:
- VCPKG_ROOT
- ANDROID_NDK_HOME(Android)
cmake --workflow --preset debugInstall asyncio from the vcpkg private registry:
-
Create a
vcpkg-configuration.jsonfile in the project root directory:{ "registries": [ { "kind": "git", "repository": "https://github.com/Hackerl/vcpkg-registry", "baseline": "8f3fe133714b097bf30a3a7ce40c522ce13dd58c", "packages": [ "asyncio", "zero" ] } ] }The
baselinedefines the minimum version ofasynciothat will be installed. The one used above might be outdated, so please update it as necessary. -
Create a
vcpkg.jsonfile in the project root directory:{ "name": "project name", "version-string": "1.0.0", "builtin-baseline": "84bab45d415d22042bd0b9081aea57f362da3f35", "dependencies": [ "asyncio" ] } -
Add the following to the
CMakeLists.txtfile:find_package(asyncio CONFIG REQUIRED) target_link_libraries(custom_target PRIVATE asyncio::asyncio-main)
I'm using a typical TCP echo server to demonstrate the features of asyncio as much as possible.
#include <asyncio/net/stream.h> // Streaming network components
#include <asyncio/thread.h> // Thread and thread pool components
#include <asyncio/signal.h> // Signal component
#include <asyncio/time.h> // Time component
#include <zero/cmdline.h> // Command line parsing component
#include <zero/formatter.h> // Formatting utilities
#include <zero/os/resource.h> // Operating system fd/handle wrapper
#ifdef _WIN32
#include <zero/os/windows/error.h> // Windows API call wrapper
#endif
namespace {
// Receive event or signal, print the task's call stack.
// For the top-level task, complex subtasks will branch out and form a tree.
asyncio::task::Task<void> tracing(const auto &task) {
#ifdef _WIN32
const zero::os::Resource event{CreateEventA(nullptr, false, false, "Global\\AsyncIOBacktraceEvent")};
if (!event)
throw co_await asyncio::error::StacktraceError<std::system_error>::make(
static_cast<int>(GetLastError()),
std::system_category()
);
while (true) {
bool cancelled{false};
// `WaitForSingleObject` cannot be integrated into EventLoop, so we use a separate thread to call it,
// and a custom cancellation function allows it to be seamlessly integrated into coroutine management.
co_await asyncio::toThread(
[&, &cancelled = std::as_const(cancelled)] {
if (WaitForSingleObject(*event, INFINITE) != WAIT_OBJECT_0)
throw zero::error::StacktraceError<std::system_error>{
static_cast<int>(GetLastError()), std::system_category()
};
if (cancelled)
throw zero::error::StacktraceError<std::system_error>{asyncio::task::Error::Cancelled};
},
[&](std::thread::native_handle_type) -> std::expected<void, std::error_code> {
cancelled = true;
return zero::os::windows::expected([&] {
return SetEvent(*event);
});
}
);
// Print the task's formatted call stack
fmt::print(stderr, "{}\n", task.trace());
}
#else
// On UNIX, the Signal component can be used directly.
auto signal = asyncio::Signal::make();
while (true) {
zero::error::guard(co_await signal.on(SIGUSR1));
// Print the task's formatted call stack
fmt::print(stderr, "{}\n", task.trace());
}
#endif
}
asyncio::task::Task<void> doSomething() {
using namespace std::chrono_literals;
while (true) {
zero::error::guard(co_await asyncio::sleep(1s));
// Trace back the coroutine call stack
fmt::print("Do some thing: {}\n", fmt::join(co_await asyncio::task::backtrace, "\n"));
}
}
asyncio::task::Task<void> handle(asyncio::net::TCPStream stream) {
fmt::print("Connection: {}\n", zero::error::guard(stream.remoteAddress()));
while (true) {
std::string message;
message.resize(1024);
const auto n = zero::error::guard(co_await stream.read(std::as_writable_bytes(std::span{message})));
if (n == 0)
break;
message.resize(n);
fmt::print("Received message: {}\n", message);
zero::error::guard(co_await stream.writeAll(std::as_bytes(std::span{message})));
}
}
asyncio::task::Task<void> serve(asyncio::net::TCPListener listener) {
std::expected<void, std::error_code> result;
// By adding each dynamic task to a `TaskGroup`,
// we can cancel them all at once and wait for them during graceful shutdown,
// ensuring that no resources or subtasks are leaked.
asyncio::task::TaskGroup group;
while (true) {
auto stream = co_await listener.accept();
if (!stream) {
result = std::unexpected{stream.error()};
break;
}
auto task = handle(*std::move(stream));
group.add(task);
// Since the `TaskGroup` doesn't care about the results of the subtasks, we can use future to bind callbacks.
// Callback binding is very flexible, just like JavaScript's Promise.
task.future().fail([](const auto &e) {
fmt::print(stderr, "Unhandled exception: {}\n", e);
});
}
// This function waits for all tasks in the `TaskGroup`.
// When the parent task is canceled, all tasks in the group will be automatically canceled here and will wait for them to complete.
co_await group;
zero::error::guard(std::move(result));
}
}
asyncio::task::Task<void> asyncMain(const int argc, char *argv[]) {
zero::Cmdline cmdline;
cmdline.add<std::string>("ip", "IP address to bind");
cmdline.add<std::uint16_t>("port", "Port number to listen on");
cmdline.parse(argc, argv);
const auto ip = cmdline.get<std::string>("ip");
const auto port = cmdline.get<std::uint16_t>("port");
auto listener = zero::error::guard(asyncio::net::TCPListener::listen(ip, port));
auto signal = asyncio::Signal::make();
// This is the main task of our program.
auto task = race(
// A TCP server was started, along with a `doSomething` task to do something else.
// They are aggregated by `all`, just like `Promise.all` in JavaScript, where failure is returned if either task fails, and the remaining tasks are canceled.
all(
serve(std::move(listener)),
doSomething()
),
// We wait for the `SIGINT` signal to gracefully shut down.
// `race` will use the result of the task that completes fastest, so when the signal arrives,
// the task is complete, `race` returns success and cancels the remaining subtasks.
asyncio::task::spawn([&]() -> asyncio::task::Task<void> {
zero::error::guard(co_await signal.on(SIGINT));
})
);
// Debugging coroutines is always difficult, so we use the built-in traceback functionality of `asyncio` to assist us.
co_await race(
task,
tracing(task)
);
}Start the server with
./server 0.0.0.0 8000, and gracefully exit by pressingctrl + cin the terminal. You can also send signal or event to trace the call tree.
For more examples, please refer to the Documentation
- HTTP Server
See the open issues for a full list of proposed features (and known issues).
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Distributed under the Apache 2.0 License. See LICENSE for more information.
Hackerl - @Hackerl - patteliu@gmail.com
Project Link: https://github.com/Hackerl/asyncio