ioring
nimony/lib/std/ioring.nim
proc initIoRing()Bring the default ring up. Idempotent, and it has to be: this module already initialises the ring at import time, so a second call — the usage example above tells callers to make one — would otherwise re-run
initOpQueues/initSlots/gCq = newSeqwhile worker threads are live insidepoll, holding indices into the seqs being replaced. That is a use-after-free plus the loss of every op in flight.proc shutdown()Stop the pool first, then tear the backend down:
closecloses the epoll/kqueue/io_uring descriptors the workers poll, so closing them while a worker is still insidepollleaves it waiting on — or re-registering against — a descriptor number the OS is free to hand to something else.Call it from a non-worker thread: it joins the workers, and a worker that joins itself deadlocks. It also stops the pool for everyone (
std/parforincluded), and the ring cannot be brought back up afterwards.proc submitNop(deadline: Deadline; cont: Continuation; resPtr: ptr int64): uint32proc submitTimeout(deadline: Deadline; cont: Continuation; resPtr: ptr int64): uint32Complete once
deadlinepasses, with no I/O at all. Unlike every other op, reaching the deadline is this one's success: it completes with0rather thanIoTimedOut.This is how a loop gets a turn on a schedule —
next(e, deadline)in the HTTP design is one of these plus whatever else is pending.proc submitRead(fd: int32; buf: pointer not nil; len: int64; deadline: Deadline; cont: Continuation; resPtr: ptr int64): uint32proc submitWrite(fd: int32; buf: pointer not nil; len: int64; deadline: Deadline; cont: Continuation; resPtr: ptr int64): uint32proc submitAccept(listenFd: int32; deadline: Deadline; cont: Continuation; resPtr: ptr int64; peer: ptr Sockaddr_storage): uint32Accept one connection. Completes with the accepted fd, or a negative result.
peer, when given, receives the address that connected — the kernel fills it as part of the accept, so asking costs no syscall. It is written only when the accept succeeds, and it must outlive the op: the completion writes through it from whichever lane ran the accept.Without it the address is dropped, which is what an access log, a rate limiter and every
X-Forwarded-Fortrust decision need and cannot reconstruct afterwards.proc submitConnect(fd: int32; sa: Sockaddr_storage; saLen: uint32; deadline: Deadline; cont: Continuation; resPtr: ptr int64): uint32Connect
fdtosa. Completes with0on success, or the negated errno — a refused connection is-ECONNREFUSED, not a generic -1, because the caller usually wants to tell "nobody listening" from "the network ate it".fdmust already be non-blocking (setNonBlocking). The attempt is started on the polling thread, not here, so that the fd is being watched from the moment it is connecting.A connect with no deadline is the classic way to hold a slot forever: a SYN into a black hole never answers. Hence the parameter, and hence no default for it.
proc submitPollAdd(fd: int32; deadline: Deadline; events: set[IoEvent]; cont: Continuation; resPtr: ptr int64): uint32Register oneshot readiness interest in
fdwithout issuing any I/O. When the fd becomes ready in one of theeventsdirections a single completion fires whoseopisopPollAddand whosereadyEventsare the directions that fired. UnlikesubmitRead/submitWrite, no transfer is performed — the caller decides what to do with the ready fd (e.g. libcurl's multi-socket engine). This is oneshot:completefrees the slot, so re-arm by callingsubmitPollAddagain after handling the event.Pass the direction you actually want. The default watches both, which is right for a probe with no preference — but a caller waiting to read a socket is woken by mere writability on every arm (a connected socket is writable nearly always), and because the op is oneshot its re-arm then spins as fast as the loop can poll. libcurl's multi-socket engine always states its direction (
CURL_POLL_IN/CURL_POLL_OUT); pass it through.A
resPtrreceives the same directions as a bit mask instead of a set, being aptr int; decode it withtoIoEvents.proc pollCompletions(comps: var openArray): int64Non-blocking: drive this lane's backend once — issue the ops queued on it and collect whatever the kernel has finished — then hand back up to
comps.lencompletions from the shared queue. Returns 0 when nothing has completed. It used to only drain the queue, so a caller that submitted and then polled had not issued anything, and acloseFdin between found no slot to cancel.proc waitCompletions(comps: var openArray): int64pollCompletionsuntil at least one completion has landed.const F_GETFL: int32const F_SETFL: int32const O_NONBLOCK: int32proc setNonBlocking(fd: int32)proc closeFdRaw(fd: int32)proc closeFd(fd: int32)Close
fd: cancel this lane's in-flight ops on it (seecancelPendingOps), deregister it from the backend, then close(2).const AF_INET: int32const SOCK_STREAM: int32const IPPROTO_TCP: int32const SOL_SOCKET: int32const SO_REUSEADDR: int32const INADDR_ANY: uint32proc socketNonBlocking(): int32A non-blocking TCP socket, which is what
submitConnectrequires: a blocking one would finish the connect inside the syscall and there would be nothing for the ring to wait on.proc loopbackAddr(sa: var Sockaddr_storage; saLen: var uint32; port: uint16)Fill
sawith127.0.0.1:port, ready forsubmitConnect.proc boundPort(fd: int32): uint16The port
fdis actually bound to. WithlistenTcp(0)the kernel picks one, and asking for it afterwards is the only way a test can listen without inventing a fixed number that a parallel run — or a socket still in TIME_WAIT — will collide with.proc listenTcp(port: uint16; backlog: int64): int32