An asynchronous event loop written in C3. Proactor-style (submit an operation, get a completion callback), zero-allocation on the hot path, with all operation storage owned by the caller.
nova gives you one event loop that drives timers, sockets, pipes, child
processes, signals, filesystem work and DNS through a single completion model:
- You submit an operation against caller-owned storage.
- The loop runs it on the platform backend (
epollorio_uringon Linux,kqueueon macOS). - When it finishes, your callback runs on the loop's owner thread.
There is no per-operation allocation: every operation embeds an intrusive
runtime header and is stored wherever you put it. The loop only allocates during
init.
- TCP: listen, accept, connect, read, write, shutdown, and socket options.
- UDP: bind, connect, send, recv, multicast join/leave, and source filters.
- Pipes: anonymous pipe pairs and adoption of existing descriptors.
- Timers: one-shot and finite-repeat timers backed by an intrusive binary heap.
- Signals: async-signal-safe delivery on the owner thread.
- Processes: spawn with stdio pipes, env/cwd control, kill, and reaping.
- Filesystem: open, read, write, stat, copyfile, and sendfile on a worker pool.
- fs-poll: watch a path for changes by polling its metadata.
- fd watch: readiness notifications for readable, writable, hangup, or error states.
- DNS: forward and reverse resolution on the worker pool.
- Work pool: run blocking or CPU-heavy work and complete it on the loop.
Two connection helpers sit over tcp::Stream, both zero-allocation after init:
nova::buffered:StreamConnuses a ring buffer for byte-stream and line protocols: bytes flow through a fixed ring, framed withfind_byte, with coalesced vectored writes and read/write watermarks.nova::framed:FramedConnuses a linear buffer with compaction for message frame protocols: contiguouspeek/reservefor in-place zero-copy response building, plusread_intoto stream a large body straight into caller storage without buffering it.
A repeating timer (Duration is microseconds; repeat = 2 means 1 initial fire
plus 2 repeats):
import std::io;
import nova @norecurse, nova::timer;
fn void on_tick(Loop* loop, timer::TimerOp* op, timer::TimerResult* result, void* userdata)
{
(void)loop; (void)op; (void)userdata;
if (result.error.status) return;
(void)io::printfn("tick (%d expirations)", result.expirations);
}
fn void main()
{
Loop loop;
timer::TimerOp op;
loop.init()!!;
defer loop.destroy();
op.init();
defer op.destroy();
timer::start(&loop, &op, { .interval = (Duration)100_000, .repeat = 2 }, &on_tick, null)!!;
nova::run(&loop, RunMode.UNTIL_STOPPED)!!;
}Prints tick (1 expirations) three times, then exits when the loop drains.
See examples/ for TCP/UDP echo servers, an HTTP server, a worker
pool, cat, a file watcher, process spawning and pipes.
- Proactor, not reactor. You submit a complete operation and receive a completion; you do not get raw readiness events to handle yourself.
- Caller-owned storage. Operation handles (
TimerOp,tcp::ReadOp, and others) live wherever you allocate them: on the stack, in a struct field, or in a pool. The library never takes ownership and never allocates per operation. - Zero-allocation hot path. Allocation happens only in
*.init. Submit, dispatch and callback paths allocate nothing (enforced by tracking-allocator tests). - Single owner thread. The loop runs on one thread; cross-thread work is
posted in and dispatched back on the owner. Calls from another thread are
rejected with
nova::NOT_OWNER. - Contracts. Caller obligations are
@require/@ensurecontracts; recoverable failures arefaultvalues viaT?; release-critical invariants usealways_assert.
nova is a C3 library bundle (nova.c3l/). Put it on a dependency search path
and depend on the nova library:
{
"dependency-search-paths": [ "lib" ],
"dependencies": [ "nova" ]
}(place nova.c3l/ under lib/), then in your code:
import nova;
import nova::timer; // and the submodules you use: net, net::tcp, net::udp, fs, ...For a standalone file: c3c compile-run main.c3 --libdir lib --lib nova.
Requires the c3c compiler.
c3c test # run the test suite (257 tests)
cd examples && c3c build all --trust=full # build every example| Target | Shows |
|---|---|
timer |
periodic + one-shot timers |
tcp-echo |
TCP accept → read → echo |
ping-pong |
client request cancelled by a one-shot timer timeout (server sleeps) |
udp-echo |
UDP recv → echo to peer |
buffered-echo |
line echo over the ring StreamConn (nova::buffered) |
framed-echo |
length-prefixed message echo over FramedConn (nova::framed), incl. read_into for large bodies |
http / http-multithread / http-signal |
HTTP server variants |
work-pool |
offload compute + cancel a queued job |
fs-cat |
stream a file to stdout |
fs-watch |
detect file change via fs-poll |
watch |
readiness-watch a raw fd (nova::watch) with one-shot re-arm |
process |
spawn a child with stdio pipes |
pipe |
anonymous pipe read/write |
dns |
forward/reverse resolution |
Design, API shape and test coverage were informed by studying three mature event loops: libuv, libxev and libevent.
See LICENSE.