Skip to content

Hackerl/asyncio

Repository files navigation

Contributors Forks Stargazers Issues Apache 2.0 License


asyncio

C++23 coroutine network framework
Explore the docs »

View Demo · Report Bug · Request Feature

Table of Contents
  1. About The Project
  2. Getting Started
  3. Usage
  4. Roadmap
  5. Contributing
  6. License
  7. Contact
  8. Acknowledgments

About The Project

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 asynciotask.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.

(back to top)

Built With

  • CMake
  • vcpkg
  • C++23

(back to top)

Getting Started

Prerequisites

Required compiler:

  • GCC >= 15
  • LLVM >= 18
  • MSVC >= 19.38

Export environment variables:

  • VCPKG_ROOT
  • ANDROID_NDK_HOME(Android)

Build

cmake --workflow --preset debug

(back to top)

Installation

Install asyncio from the vcpkg private registry:

  1. Create a vcpkg-configuration.json file in the project root directory:

    {
      "registries": [
        {
          "kind": "git",
          "repository": "https://github.com/Hackerl/vcpkg-registry",
          "baseline": "8f3fe133714b097bf30a3a7ce40c522ce13dd58c",
          "packages": [
            "asyncio",
            "zero"
          ]
        }
      ]
    }

    The baseline defines the minimum version of asyncio that will be installed. The one used above might be outdated, so please update it as necessary.

  2. Create a vcpkg.json file in the project root directory:

    {
      "name": "project name",
      "version-string": "1.0.0",
      "builtin-baseline": "84bab45d415d22042bd0b9081aea57f362da3f35",
      "dependencies": [
        "asyncio"
      ]
    }
  3. Add the following to the CMakeLists.txt file:

    find_package(asyncio CONFIG REQUIRED)
    target_link_libraries(custom_target PRIVATE asyncio::asyncio-main)

Usage

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 pressing ctrl + c in the terminal. You can also send signal or event to trace the call tree.

For more examples, please refer to the Documentation

(back to top)

Roadmap

  • HTTP Server

See the open issues for a full list of proposed features (and known issues).

(back to top)

Contributing

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!

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

(back to top)

License

Distributed under the Apache 2.0 License. See LICENSE for more information.

(back to top)

Contact

Hackerl - @Hackerl - patteliu@gmail.com

Project Link: https://github.com/Hackerl/asyncio

(back to top)

Acknowledgments

(back to top)

About

C++23 coroutine network framework

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors