Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nova

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.

Overview

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 (epoll or io_uring on Linux, kqueue on 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.

Features

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

Buffering layers (optional)

Two connection helpers sit over tcp::Stream, both zero-allocation after init:

  • nova::buffered: StreamConn uses a ring buffer for byte-stream and line protocols: bytes flow through a fixed ring, framed with find_byte, with coalesced vectored writes and read/write watermarks.
  • nova::framed: FramedConn uses a linear buffer with compaction for message frame protocols: contiguous peek/reserve for in-place zero-copy response building, plus read_into to stream a large body straight into caller storage without buffering it.

Example

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.

Design

  • 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/@ensure contracts; recoverable failures are fault values via T?; release-critical invariants use always_assert.

Install

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.

Build & test

Requires the c3c compiler.

c3c test                          # run the test suite (257 tests)
cd examples && c3c build all --trust=full   # build every example

Examples

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

Acknowledgments

Design, API shape and test coverage were informed by studying three mature event loops: libuv, libxev and libevent.

License

See LICENSE.

About

An asynchronous event loop written in C3.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages