🔗Event loop
The Loop is the heart of powpow. Everything — TCP, UDP, HTTP, WebSocket, DNS, signals, file watching, timers — is driven by a single Loop. It is a single-threaded non-blocking reactor over the platform's native multiplexer (kqueue on macOS/BSD, epoll on Linux, IOCP on Windows, poll as a fallback). On Linux the same loop can instead use the opt-in io_uringbackend, where I/O is driven by submission/completion ops rather than readiness events — see the io_uring guide.
Runnable example: examples/timers_scheduler.nim.
🔗Creating a loop
let loop = newLoop()Most higher-level helpers can create their own loop for you (newHttpServer(), newWsServer()), but passing an explicit Loop is how you compose several services — HTTP, DNS, signals, timers — into one process and one thread.
🔗The main loop
loop.run() # blocks forever, dispatching events
loop.runOnce() # run a single iteration of the poll loop
loop.poll(timeoutMs = -1)
loop.stop() # thread-safe: wake the loop and stop it
loop.isRunning() # is the loop still active?stop() is thread-safe (via eventfd on Linux, a self-pipe on macOS/BSD), so another thread or a signal handler can ask the loop to exit.
🔗Monotonic clock
let now = monoMs() # int64 milliseconds since an arbitrary epoch🔗Timers and the timer wheel
Timers are backed by a 4-level hierarchical timer wheel with O(1) insert/fire/cancel.
proc onTimer(id: int) = echo "fired"
let oneShot = loop.addTimer(1000, onTimer) # fire once after 1s
let interval = loop.addInterval(500, onTimer) # fire every 500ms
loop.cancelTimer(oneShot) # never fire
loop.pauseTimer(interval) # pause (keeps remaining time)
loop.resumeTimer(interval) # resume
loop.timerCount() # number of active timersTimer callbacks receive the timer's id (TimerId, a distinct int).
🔗Deferred callbacks
Run before the next poll iteration — the "do this as soon as the current event handling unwinds" queue.
loop.deferCall(proc() = echo "runs before the next poll")🔗Cross-thread posting
postToLoop is the safe way to get work onto the loop from another thread.
# from any thread:
loop.postToLoop(proc() = echo "runs on the loop thread")This is the primitive the thread-safety tests and multi-threaded server rely on.
🔗Idle handlers
Run when the loop has no I/O events to process.
let idleId = loop.addIdle(proc() = echo "nothing to do")
loop.removeIdle(idleId)🔗Observers
Watch a uint64 value for changes and get a callback.
var counter: uint64
let obs = loop.observe(addr counter, proc(value: uint64) = echo value)
counter += 1 # next loop tick notices and fires the observer
loop.cancelObserver(obs)🔗File descriptor events
register wires an fd into the loop with a callback for a set of events. This is the raw API the higher-level net modules are built on.
import powpow
proc onFd(fd: int, events: set[EventType]) =
if Read in events:
# read from fd, then re-arm if you are edge-triggered
discard
loop.register(fd, {Read, Error, Hup}, onFd, edgeTriggered = true)
loop.modify(fd, {Read, Write, Error, Hup}) # change the interest set
loop.unregister(fd)EventType is Read, Write, Error or Hup. Watchers are dispatched by pointer with generation-counter stale-event detection, so a callback is never invoked for an fd that was unregistered and reused.
🔗Cleanup hooks
loop.addCleanup(proc() = echo "runs when the loop closes")loop.close() runs all cleanup hooks (the DNS resolver, the signal source and the file watchers register their own) and closes the platform backend.
🔗Public Loop fields
platform*: Platform— the active I/O backendbufPool*: seq[ptr UncheckedArray[byte]]— the shared read-buffer pooldns*: ref RootObj— the DNS resolver, when createdsigSource*: ref RootObj— the OS-signal source, when createdclosed*: bool
🔗Buffer pooling
The loop carries a pool of shared read buffers used by the TCP layer:
let buf = loop.acquireBuf() # ptr UncheckedArray[byte]
# ... read into it ...
loop.releaseBuf(buf)DefaultBufSize is 4096 bytes.
🔗Backends
The platform layer is selected at compile time by platform.nim:
| Platform | Backend |
|---|---|
| Linux | epoll (with eventfd wake), or opt-in io_uring ( -d:powpowIoUring) |
| macOS / BSD | kqueue (edge-triggered) |
| Windows | iocp |
| anything else | poll |
Every backend exposes the same surface: Platform.init, add, remove, modify, wake, poll, close, plus the Platform/PlatformEvent types. You almost never touch these directly — see the platform API page.
The io_uring backend shares this same Loop type and the timer wheel/deferred/idle machinery; only the I/O driving differs (submission-based ops instead of readiness events). See the io_uring guide.