SkillAgentSearch skills...

Coroio

Networking library using C++ coroutines

Install / Use

/learn @resetius/Coroio
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

COROIO

<img src="/bench/logo.png?raw=true" width="400"/>

C++23 coroutine-based async I/O library. Single-threaded event loop, zero virtual dispatch on the hot path, pluggable polling backends.

Stars Commits License


Architecture

TLoop<TPoller>
  └── TPoller                    ← TEPoll | TKqueue | TIOCp | TUring | TPoll | TSelect
        ├── TSocket              ← async TCP/UDP socket
        ├── TFileHandle          ← async file descriptor
        └── TPollerBase          ← timers, Sleep(), Yield(), AddTimer()

TDefaultPoller resolves to TEPoll on Linux, TKqueue on macOS/FreeBSD, TIOCp on Windows.

All networking types are obtained as TPoller::TSocket / TPoller::TFileHandle so they bind to the correct backend at compile time.

I/O primitives

| Type | Description | |---|---| | TSocket | Async network socket. Connect, Accept, Bind, Listen, ReadSome, WriteSome | | TFileHandle | Async file descriptor (including stdin/stdout). Same ReadSome/WriteSome interface | | TByteReader(sock) | Reads exactly N bytes; ReadUntil(delim) for delimiter-terminated reads | | TByteWriter(sock) | Writes exactly N bytes | | TLineReader(fd, maxSize) | Line-by-line from a circular buffer → TLine{Part1, Part2} |

ReadSome/WriteSome return bytes transferred, 0 on close, -1 on transient error. TLine has two parts because the internal circular buffer may wrap around.


Coroutine types

TVoidTask           fire-and-forget   cannot be co_await-ed, self-destructs, swallows exceptions
TFuture<void>       owned task        can be co_await-ed, propagates exceptions, RAII handle lifetime
TFuture<T>          owned task        same + carries a return value of type T

TVoidTask — detached coroutine. The caller does not own it; it runs independently and destroys itself on completion. Exceptions are silently dropped. Use for spawned handlers that outlive their creator (e.g. per-connection tasks in an accept loop).

TFuture<T> — owned coroutine. The caller holds the handle. co_await future suspends the caller until the coroutine finishes, then returns T (or rethrows a stored exception). future.done() polls completion without suspending. The handle is destroyed in ~TFuture.

// TVoidTask: spawn and forget — no co_await possible
TVoidTask handle_connection(TSocket sock) {
    char buf[4096]; ssize_t n;
    while ((n = co_await sock.ReadSome(buf, sizeof(buf))) > 0)
        co_await sock.WriteSome(buf, n);
    // exceptions here are swallowed
}

// TFuture<T>: await the result
TFuture<int> read_int(TSocket& sock) {
    int v; co_await TByteReader(sock).Read(&v, sizeof(v));
    co_return v;
}

TFuture<void> use_it(TSocket& sock) {
    int v = co_await read_int(sock);   // suspends until read_int finishes
    // exception from read_int propagates here
}

Running a TFuture from main — drive the loop until .done():

int main() {
    NNet::TInitializer init;
    TLoop<TDefaultPoller> loop;
    TFuture<void> task = my_coroutine(loop.Poller(), ...);
    while (!task.done())
        loop.Step();
}

Running a TVoidTask from main — the task is detached, so drive the loop by another condition:

int main() {
    NNet::TInitializer init;
    TLoop<TDefaultPoller> loop;
    server(loop.Poller(), TAddress{"::", 8888});  // returns TVoidTask, immediately detached
    loop.Loop();                                   // runs forever (until loop.Stop())
}

Combining futures

// Wait for all — sequentially awaits each future in order
TFuture<std::vector<int>> f = All(std::move(futures));   // TFuture<vector<T>>
TFuture<void>             f = All(std::move(void_futures));

// Wait for any — resumes when the first one finishes
TFuture<int>  f = Any(std::move(futures));
TFuture<void> f = Any(std::move(void_futures));

// Transform a result without co_await at call site
TFuture<std::string> f = read_int(sock).Apply([](int v) { return std::to_string(v); });

// Discard the return value
TFuture<void> f = read_int(sock).Ignore();

// Run a continuation after a void future
TFuture<void> f = wait_for_event().Accept([] { cleanup(); });

Usage

Include <coroio/all.hpp>.

Echo server

#include <coroio/all.hpp>
using namespace NNet;

template<typename TSocket>
TVoidTask client_handler(TSocket socket) {
    char buf[4096]; ssize_t n;
    while ((n = co_await socket.ReadSome(buf, sizeof(buf))) > 0)
        co_await socket.WriteSome(buf, n);
}

template<typename TPoller>
TVoidTask server(TPoller& poller, TAddress addr) {
    typename TPoller::TSocket sock(poller, addr.Domain());
    sock.Bind(addr); sock.Listen();
    while (true)
        client_handler(co_await sock.Accept());
}

int main() {
    NNet::TInitializer init;
    TLoop<TDefaultPoller> loop;
    server(loop.Poller(), TAddress{"::", 8888});
    loop.Loop();
}

Echo client (stdin → server → stdout)

template<typename TPoller>
TFuture<void> client(TPoller& poller, TAddress addr) {
    using TSocket     = typename TPoller::TSocket;
    using TFileHandle = typename TPoller::TFileHandle;
    char in[4096];

    TFileHandle input{0, poller};          // stdin
    TSocket socket{poller, addr.Domain()};
    TLineReader  lineReader(input, 4096);
    TByteWriter  byteWriter(socket);
    TByteReader  byteReader(socket);

    co_await socket.Connect(addr, TClock::now() + std::chrono::seconds(1));
    while (auto line = co_await lineReader.Read()) {
        co_await byteWriter.Write(line);
        co_await byteReader.Read(in, line.Size());
        std::cout << std::string_view(in, line.Size());
    }
}

Selecting a backend explicitly

TLoop<TEPoll>  loop;   // Linux epoll
TLoop<TUring>  loop;   // Linux io_uring
TLoop<TKqueue> loop;   // macOS / FreeBSD
TLoop<TIOCp>   loop;   // Windows IOCP
TLoop<TSelect> loop;   // portable fallback

Utilities

DNS (<coroio/dns/resolver.hpp>)

TResolvConf          reads /etc/resolv.conf (or any istream) → .Nameservers
TResolver(poller)    async DNS over UDP; caches results; retries on timeout
THostPort(host,port) resolves hostname or passes through a literal IP
TResolver resolver(loop.Poller());                          // uses /etc/resolv.conf

auto addrs = co_await resolver.Resolve("example.com");      // A record (IPv4)
auto addrs = co_await resolver.Resolve("example.com", EDNSType::AAAA); // IPv6

// THostPort skips DNS for literal IPs
TAddress addr = co_await THostPort("example.com", 80).Resolve(resolver);

TResolver lives for the duration of the loop and handles multiple concurrent requests; call Resolve from multiple coroutines simultaneously.


HTTP server (<coroio/http/httpd.hpp>)

TWebServer<TSocket>          accept loop + per-connection handler
IRouter                      implement HandleRequest(TRequest&, TResponse&)
TRequest                     method, URI, headers, body (Content-Length or chunked)
TResponse                    SetStatus, SetHeader, SendHeaders, WriteBodyFull / WriteBodyChunk
TUri                         path, query parameters, fragment
struct MyRouter : NNet::IRouter {
    TFuture<void> HandleRequest(TRequest& req, TResponse& res) override {
        res.SetStatus(200);
        res.SetHeader("Content-Type", "text/plain");
        co_await res.SendHeaders();
        co_await res.WriteBodyFull("hello");
        // or streaming: co_await res.WriteBodyChunk(data, size);
    }
};

// startup
using TSocket = TDefaultPoller::TSocket;
TLoop<TDefaultPoller> loop;
TSocket sock(loop.Poller(), addr.Domain());
sock.Bind(addr); sock.Listen();
MyRouter router;
TWebServer<TSocket> server(std::move(sock), router);
server.Start();
loop.Loop();

Reading the request body:

std::string body = co_await req.ReadBodyFull();        // slurp entire body
ssize_t n = co_await req.ReadBodySome(buf, size);      // streaming

WebSocket (<coroio/ws/ws.hpp>)

TWebSocket<TSocket> wraps any connected socket with the WebSocket framing layer. Client side only (server upgrade not included). Text frames only.

typename TPoller::TSocket sock(poller, addr.Domain());
co_await sock.Connect(addr);

TWebSocket ws(sock);
co_await ws.Connect("example.com", "/chat");   // HTTP upgrade handshake

co_await ws.SendText("hello");
std::string_view msg = co_await ws.ReceiveText();

TWebSocket holds a reference to the socket — the socket must outlive it.


SSL/TLS (<coroio/ssl.hpp>, requires OpenSSL)

TSslContext::Client(logFn)                    client context
TSslContext::Server(certfile, keyfile, logFn) server context (PEM files)
TSslContext::ServerFromMem(cert*, key*, logFn) server context (in-memory PEM)
TSslSocket<TSocket>(socket, ctx)              TLS layer; same ReadSome/WriteSome interface
// TLS client
TSslContext ctx = TSslContext::Client();
TSslSocket ssl(std::move(socket), ctx);
ssl.SslSetTlsExtHostName("example.com");  // SNI
co_await ssl.Connect(addr);
ssize_t n = co_await ssl.ReadSome(buf, size);

// TLS server
TSslContext ctx = TSslContext::Server("server.crt", "server.key");
TSslSocket ssl(std::move(accepted_socket), ctx);
co_await ssl.AcceptHandshake();

TSslSocket is detected by presence of <openssl/bio.h> at compile time (HAVE_OPENSSL). TWebSocket over TLS works by wrapping TSslSocket instead of a plain socket.


Pipe (<coroio/pipe/pipe.hpp>, Linux/macOS only)

Spawns a child process and exposes its stdin/stdout/stderr as async handles.

TPipe pipe(poller, "/bin/cat", {});           // exe + args
TPipe pipe(poller, "/bin/bash", {"-c", "..."}, /*stderrToStdout=*/tr

Related Skills

View on GitHub
GitHub Stars117
CategoryDevelopment
Updated6d ago
Forks21

Languages

C++

Security Score

100/100

Audited on Mar 22, 2026

No findings