https://github.com/ejunjsh/cpp-coroutine-epoll
Small Linux/macOS networking example that wraps readiness events with C++20 coroutines.
https://github.com/ejunjsh/cpp-coroutine-epoll
coroutine cpp cpp20 epoll kqueue
Last synced: 5 days ago
JSON representation
Small Linux/macOS networking example that wraps readiness events with C++20 coroutines.
- Host: GitHub
- URL: https://github.com/ejunjsh/cpp-coroutine-epoll
- Owner: ejunjsh
- License: apache-2.0
- Created: 2026-06-01T06:38:35.000Z (28 days ago)
- Default Branch: main
- Last Pushed: 2026-06-19T06:22:15.000Z (10 days ago)
- Last Synced: 2026-06-19T07:26:26.562Z (10 days ago)
- Topics: coroutine, cpp, cpp20, epoll, kqueue
- Language: C++
- Homepage:
- Size: 62.5 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# C++20 Coroutine Event Loop

A C++20 coroutine-based networking library that wraps readiness events with `co_await`. Linux uses `epoll` + `eventfd`; macOS uses `kqueue` + `pipe`. All I/O is non-blocking, and coroutines are suspended/resumed by the event loop without any callback nesting.
## Library
### `coro_epoll::Task`
The coroutine return type. A `Task` bridges the synchronous world (event loop) with `co_await`-based async code.
- `Task` for coroutines that produce a value; `Task` for fire-and-forget or side-effect-only coroutines.
- Move-only: each `Task` uniquely owns a coroutine handle. Destroying a `Task` destroys the coroutine.
- `co_await task` suspends the caller and resumes when the inner coroutine completes, propagating the result (or exception).
- `promise_type::initial_suspend()` returns `suspend_always` — newly created tasks are lazily started via `EventLoop::spawn()`.
### `coro_epoll::EventLoop`
Single-threaded reactor. Owns one `epoll` (Linux) or `kqueue` (macOS) fd.
| Method | Description |
|--------|-------------|
| `spawn(Task&&)` | Start a coroutine on this loop. |
| `post(std::function)` | Enqueue a callback from another thread. Thread-safe. |
| `run()` | Blocking event loop. Returns when `stop()` is called. |
| `stop()` | Signal the loop to exit. Thread-safe. |
| `readable(int fd)` | Returns an awaiter that suspends until `fd` is readable (level-triggered). |
| `writable(int fd)` | Returns an awaiter that suspends until `fd` is writable (level-triggered). |
| `readable_et(int fd)` | Edge-triggered variant of `readable`. Fires only on state change. |
| `writable_et(int fd)` | Edge-triggered variant of `writable`. Fires only on state change. |
| `remove(int fd)` | Deregister `fd` and clean up associated coroutine handles. |
`post()` enables cross-thread scheduling: a business thread can post a continuation back to the owning `EventLoop`, keeping socket I/O on the correct thread.
### `coro_epoll::WorkerGroup`
Owns N `EventLoop` instances, each running on its own thread. Provides round-robin access via `next()`.
| Method | Description |
|--------|-------------|
| `next()` | Returns the next `EventLoop&` in round-robin order. Thread-safe. |
| `stop()` / `join()` | Gracefully shut down all worker loops. |
Combined with `SO_REUSEPORT`, each worker can bind its own listening socket and the kernel distributes incoming connections.
### `coro_epoll::ThreadPool`
Runs CPU-heavy or blocking tasks off the network threads. When a task completes, the coroutine continuation is posted back to the originating `EventLoop`.
```cpp
std::string result = co_await pool.submit(loop, [] {
return expensive_computation();
});
// Resumed on 'loop' — safe to read/write sockets here.
```
### `coro_epoll::TcpServer`
Non-blocking TCP listening socket, bound to an `EventLoop`.
| Method | Description |
|--------|-------------|
| `listen(port, backlog, reuse_port)` | Create, bind, and listen. `reuse_port` enables `SO_REUSEPORT`. |
| `async_accept_fd()` | `co_await` a new client fd (level-triggered). |
| `async_accept()` | `co_await` a new `TcpSocket` (level-triggered). |
| `async_accept_et()` | Edge-triggered variant — drains accept queue on each event, returns `std::vector`. |
### `coro_epoll::TcpSocket`
Non-blocking connected TCP socket. Move-only; destructor closes the fd and deregisters from the `EventLoop`.
| Method | Description |
|--------|-------------|
| `async_read(buffer, size)` | `co_await` until data arrives (level-triggered). Returns bytes read, or `0` on EOF. |
| `async_write(buffer, size)` | `co_await` until all bytes are written. Partial writes are handled transparently. |
| `async_read_et(buffer, size)` | Edge-triggered variant — drains socket buffer until `EAGAIN` on each event. |
### `coro_epoll::UdpSocket`
Non-blocking UDP socket.
| Method | Description |
|--------|-------------|
| `bind(port, reuse_port)` | Create and bind. Supports `SO_REUSEPORT`. |
| `async_recv_from(buffer, size)` | `co_await` a datagram (level-triggered). Returns `UdpReceiveResult{size, endpoint}`. |
| `async_recv_from_et(buffer, size)` | Edge-triggered variant — drains socket buffer, returns `std::vector`. |
| `async_send_to(buffer, size, endpoint)` | `co_await` until the datagram is sent. |
### `coro_epoll::UdpEndpoint`
Immutable IPv4 address + port. Construct from `(string_view address, uint16_t port)`, or from a raw `sockaddr_in`.
## Threading model
```text
worker thread N
EventLoop N
epoll_wait(eventfd + listen fd + client fds)
accept() ← SO_REUSEPORT, kernel distributes connections
spawn client coroutine
resume read/write coroutines
business thread pool
run CPU-heavy / blocking tasks
post continuation back to the originating EventLoop via loop.post()
```
Each `EventLoop` owns its `epoll`/`kqueue` fd and runs on a dedicated thread. A client socket stays on whichever worker accepted it — all subsequent I/O for that socket is handled by the same worker, so no locking is needed for socket state.
## Build
```bash
cmake -S . -B build
cmake --build build
```
## Examples
- [tcp_echo_server](examples/tcp-echo-server/README.md) — TCP echo server (level-triggered)
- [tcp_echo_server_et](examples/tcp-echo-server-et/README.md) — TCP echo server (edge-triggered)
- [udp_echo_server](examples/udp-echo-server/README.md) — UDP echo server (level-triggered)
- [udp_echo_server_et](examples/udp-echo-server-et/README.md) — UDP echo server (edge-triggered)
- [proxy_server](examples/proxy-server/README.md) — TCP proxy
- [http_server](examples/http-server/README.md) — Minimal HTTP/1.1 server