Low-level, non-blocking I/O for C++26. Linux-first, edge-triggered, zero-cost abstractions over epoll.
Part of the tehrun project.
tio is a portable event-driven I/O library inspired by Rust's mio. It gives you:
- Non-blocking TCP, UDP, Unix sockets, and pipes with a unified registration API
- Edge-triggered epoll behind a clean, type-safe interface
std::expected-based error handling — no exceptions, no RTTI- Zero-cost strong types for tokens, interests, and events
- Move-only RAII for all file descriptors — no leaks by design
tio is the I/O foundation layer. It does not provide async/await, task scheduling, or timers — those belong in higher-level runtime libraries built on top of tio.
- Linux (epoll backend)
- C++26 (
-std=c++2c) - Clang 19+ (primary) or GCC 14+ (secondary)
- CMake 3.30+
cmake -B build -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release
cmake --build buildRun tests (requires Google Test):
cmake --build build --target test| Option | Default | Description |
|---|---|---|
TIO_BUILD_TESTS | ON | Build unit tests |
TIO_BUILD_EXAMPLES | ON | Build example programs |
TIO_BACKEND | epoll | I/O backend (epoll, io_uring planned) |
add_subdirectory(tio)
target_link_libraries(your_targetPRIVATEtio::tio)#include<array>
#include<print>
#include<tio/tio.hpp>usingnamespacetio;usingnamespacetio::net;intmain() {
auto poll = poll::create().value();
auto listener = tcp_listener::bind(detail::socket_addr::ipv4_any(9000)).value();
auto reg = poll.get_registry();
// register the listener for readable eventsauto listener_tok = token{0};
reg.register_source(listener, listener_tok, interest::readable()).value();
events evs{1024};
std::size_t next_id = 1;
while (true) {
poll.do_poll(evs, std::nullopt).value();
for (constauto& ev : evs) {
if (ev.tok() == listener_tok) {
// accept new connections until EAGAINwhile (true) {
auto result = listener.accept();
if (!result) {
if (result.error().is_would_block()) break;
std::println(stderr, "accept: {}", result.error());
break;
}
auto& [stream, peer] = *result;
auto tok = token{next_id++};
std::println("accepted {} as {}", peer, tok);
reg.register_source(stream, tok, interest::readable()).value();
// ... store stream somewhere
}
}
}
}
}#include<array>
#include<print>
#include<tio/tio.hpp>usingnamespacetio;usingnamespacetio::net;intmain() {
auto sender = udp_socket::bind(detail::socket_addr::ipv4_any(0)).value();
auto receiver = udp_socket::bind(detail::socket_addr::ipv4_loopback(4000)).value();
auto msg = std::array<std::byte, 5>{
std::byte{'h'}, std::byte{'e'}, std::byte{'l'}, std::byte{'l'}, std::byte{'o'}
};
auto dest = detail::socket_addr::ipv4_loopback(4000);
sender.send_to(msg, dest).value();
std::array<std::byte, 1024> buf{};
auto [n, from] = receiver.recv_from(buf).value();
std::println("received {} bytes from {}", n, from);
}#include<tio/tio.hpp>usingnamespacetio;intmain() {
auto poll = poll::create().value();
auto reg = poll.get_registry();
// any type satisfying the `source` concept can register:// tcp_listener, tcp_stream, udp_socket,// unix_listener, unix_stream, unix_datagram,// pipe_sender, pipe_receiver, raw_fdauto listener = net::tcp_listener::bind(detail::socket_addr::ipv4_any(8080)).value();
reg.register_source(listener, token{0}, interest::readable()).value();
auto [tx, rx] = unix_::make_pipe().value();
reg.register_source(rx, token{1}, interest::readable()).value();
events evs{256};
while (true) {
// blocks until at least one event fires (or timeout)
poll.do_poll(evs, std::chrono::milliseconds{1000}).value();
for (constauto& ev : evs) {
if (ev.is_readable()) { /* drain the source */ }
if (ev.is_writable()) { /* flush pending writes */ }
if (ev.is_read_closed()) { /* peer hung up */ }
if (ev.is_error()) { /* check take_error() */ }
}
}
}#include<thread>
#include<tio/tio.hpp>usingnamespacetio;intmain() {
auto poll = poll::create().value();
auto waker = waker::create(poll.get_registry(), token{42}).value();
std::thread worker([waker] {
// do some work ...
waker.wake().value(); // wake the poll loop from another thread
});
events evs{64};
poll.do_poll(evs, std::nullopt).value();
for (constauto& ev : evs) {
if (ev.tok() == token{42}) {
// woken up by the worker thread
}
}
worker.join();
}| Type | Header | Description |
|---|---|---|
poll | <tio/poll.hpp> | Event loop — wraps epoll instance |
registry | <tio/poll.hpp> | Registers/deregisters sources with a poll |
events | <tio/event.hpp> | Reusable event buffer, iterable |
event | <tio/event.hpp> | Single event — query is_readable(), is_writable(), etc. |
token | <tio/token.hpp> | Strong-typed size_t identifying an event source |
interest | <tio/interest.hpp> | Bitmask: readable(), writable(), priority() |
error | <tio/error.hpp> | Errno wrapper with named predicates |
result<T> | <tio/error.hpp> | Alias for std::expected<T, error> |
waker | <tio/waker.hpp> | Thread-safe poll wakeup via eventfd |
| Type | Header | Description |
|---|---|---|
tcp_listener | <tio/net/tcp_listener.hpp> | Non-blocking TCP server socket |
tcp_stream | <tio/net/tcp_stream.hpp> | Non-blocking TCP connection |
udp_socket | <tio/net/udp_socket.hpp> | Non-blocking UDP socket with multicast |
| Type | Header | Description |
|---|---|---|
unix_listener | <tio/unix/unix_listener.hpp> | Unix domain stream listener |
unix_stream | <tio/unix/unix_stream.hpp> | Unix domain stream connection |
unix_datagram | <tio/unix/unix_datagram.hpp> | Unix domain datagram socket |
pipe_sender / pipe_receiver | <tio/unix/pipe.hpp> | Unidirectional pipe pair |
| Type | Header | Description |
|---|---|---|
raw_fd | <tio/raw_fd.hpp> | Wrap any fd (timerfd, serial, etc.) as a source |
fd_guard | <tio/sys/detail/fd_guard.hpp> | RAII fd wrapper — closes on destruction |
socket_addr | <tio/sys/detail/socket_addr.hpp> | IPv4/IPv6 address helper |
Any type that implements three methods can register with a poll:
autotio_register(const registry&, token, interest) -> void_result;
autotio_reregister(const registry&, token, interest) -> void_result;
autotio_deregister(const registry&) -> void_result;All built-in types (tcp_listener, udp_socket, pipe_receiver, raw_fd, ...) satisfy this concept.
- No exceptions, no RTTI. Compiles with
-fno-exceptions -fno-rtti. - Edge-triggered only. All epoll registrations use
EPOLLET. You must drain reads/writes untilEAGAIN. - Non-blocking by default. All sockets are created with
SOCK_NONBLOCK | SOCK_CLOEXEC. - Move-only ownership. File descriptors cannot be copied. They close on destruction.
- Compile-time backend selection. No virtual dispatch. The backend (epoll, io_uring) is chosen via CMake and resolved with
#if/using.
MIT