No matching definitions.

tur/reactor

stdlib/reactor.tur

lightweight evented I/O reactor.

Single-threaded event loop for multiplexing file descriptors, timers, OS signals, and channel receives without requiring the cooperative scheduler. Create one reactor per thread that needs to poll; reactors are not thread-safe. The only thread-safe operations are reactor-stop and reactor-wake.

Since: Phase R1

defopaque

EventSourceId

(defopaque EventSourceId :int)
defopaque linear

Reactor

(defopaque Reactor :ptr<void> :linear)
def

READ

(def READ 1)

event mask bit: fd is readable or an accept is ready.

Since: Phase R2

def

WRITE

(def WRITE 2)

event mask bit: fd is writable or a connect has completed.

Since: Phase R2

def

ERROR

(def ERROR 4)

event mask bit: error condition on the fd. Always reported; cannot be masked out at registration time.

Since: Phase R2

def

HUP

(def HUP 8)

event mask bit: the peer hung up or EOF was reached.

Since: Phase R2

defn

reactor-new

(reactor-new)

create a new reactor for the current thread.

The underlying I/O backend is chosen at compile time: epoll on Linux, kqueue on macOS/BSD. Returns nil on WASM or if the backend cannot be initialised.

ptr<void> -- opaque reactor handle, or nil on error

(let [r (reactor-new)]
  (reactor-run r)
  (reactor-free r))

Since: Phase R1

defn

reactor-free

(reactor-free [r :Reactor])

free a reactor and unregister all attached sources.

rreactor handle returned by reactor-new
(reactor-free r)

Since: Phase R1

defn

reactor-add-fd

(reactor-add-fd [^borrow r :Reactor fd :int events :int ^fat cb :(fn [int int ptr<void>] nil) user-data :ptr<void>])

watch a file descriptor for I/O events.

Returns a source id (>= 0) that uniquely identifies this registration. Pass the id to reactor-modify-fd or reactor-remove to change or cancel the watch. Returns -1 on error.

The callback is invoked as (cb source-id events user-data) each time the fd becomes ready. Events is a bitmask of READ, WRITE, ERROR, HUP. Callbacks fire on the thread that calls reactor-poll or reactor-run.

rreactor handle
fdfile descriptor to watch (must already be open)
eventsbitmask of event types to watch: READ | WRITE
cbclosure: (fn [id :int events :int user :ptr<void>] :nil)
user-dataopaque value forwarded to cb as its third argument

EventSourceId -- source id (>= 0), or -1 on error (unwrap with (:: id :int))

(reactor-add-fd r listen-fd READ
  (fn [id events user]
    (handle-accept listen-fd))
  nil)

Since: Phase R2

defn

reactor-modify-fd

(reactor-modify-fd [^borrow r :Reactor id :EventSourceId new-events :int])

change the watched event mask for a source.

rreactor handle
idsource id returned by reactor-add-fd
new-eventsreplacement event bitmask

int -- 0 on success, -1 if id is not found or the modify fails

(reactor-modify-fd r source-id WRITE)

Since: Phase R2

defn

reactor-remove

(reactor-remove [^borrow r :Reactor id :EventSourceId])

unregister a source by id.

After this call the fd is no longer watched and the callback will not fire again. The fd itself is not closed.

rreactor handle
idsource id returned by reactor-add-fd

int -- 0 on success, -1 if id is not found

(reactor-remove r source-id)

Since: Phase R2

defn

reactor-add-timer

(reactor-add-timer [^borrow r :Reactor delay-ms :int ^fat cb :(fn [int ptr<void>] nil) user-data :ptr<void>])

register a one-shot timer that fires once.

The callback fires after delay-ms milliseconds on the thread that calls reactor-poll or reactor-run. After firing the source is automatically deactivated; it does not need to be removed manually.

The callback is invoked as (cb source-id user-data).

rreactor handle
delay-msmilliseconds until the timer fires (>= 0)
cbclosure: (fn [id :int user :ptr<void>] :nil)
user-dataopaque value forwarded to cb as its second argument

EventSourceId -- source id (>= 0), or -1 on error (unwrap with (:: id :int))

(reactor-add-timer r 500
  (fn [id user] :nil (println "fired"))
  nil)

Since: Phase R4

defn

reactor-add-interval

(reactor-add-interval [^borrow r :Reactor first-ms :int interval-ms :int ^fat cb :(fn [int ptr<void>] nil) user-data :ptr<void>])

register a repeating interval timer.

The callback fires first after first-ms milliseconds, then every interval-ms milliseconds thereafter. The source remains active until reactor-remove is called with its id.

The callback is invoked as (cb source-id user-data) on each tick.

rreactor handle
first-msdelay before the first fire in milliseconds
interval-msperiod between subsequent fires in milliseconds
cbclosure: (fn [id :int user :ptr<void>] :nil)
user-dataopaque value forwarded to cb

EventSourceId -- source id (>= 0), or -1 on error (unwrap with (:: id :int))

(reactor-add-interval r 0 1000
  (fn [id user] :nil (println "tick"))
  nil)

Since: Phase R4

defn

reactor-add-signal

(reactor-add-signal [^borrow r :Reactor signum :int ^fat cb :(fn [int int ptr<void>] nil) user-data :ptr<void>])

watch an OS signal via the reactor.

Uses signalfd on Linux and a self-pipe + sigaction on macOS/BSD. The signal is masked from normal delivery while the source is active; calling reactor-remove restores the previous signal disposition. Returns -1 on WASM or if registration fails.

The callback is invoked as (cb source-id signum user-data).

rreactor handle
signumOS signal number (e.g. 2 for SIGINT)
cbclosure: (fn [id :int signum :int user :ptr<void>] :nil)
user-dataopaque value forwarded to cb

EventSourceId -- source id (>= 0), or -1 on error (unwrap with (:: id :int))

(reactor-add-signal r 2
  (fn [id sig user] :nil (println "got SIGINT"))
  nil)

Since: Phase R5

defn

reactor-add-chan

(reactor-add-chan [A])

watch a channel for one incoming value (one-shot).

Fires at most once: when a value is available the reactor dequeues it, the callback receives (source-id, value, user-data), and the source is deactivated. Re-register from the callback for a persistent channel watcher.

The channel is borrowed, but the reactor goes on reading it after this call returns: keep it alive -- do not chan-free it -- until the watch has fired, been removed with reactor-remove, or the reactor has been freed.

Delivery latency: the channel is checked on every reactor-poll / reactor-run iteration, and a channel that already holds a value never lets the poll block, so a value sent before the watch is delivered on the next poll. For prompt wakeup after a chan-send made once the watch is live, call reactor-wake from the sending thread.

The value reaches cb as the channel's carrier word, typed :int. For a (Chan int) that is the int itself. For another element type, leave the slot untyped and convert in the body: (:: v cstr) for a word-sized element, (bits->float v) for a float. Do not annotate the slot with the element type. reactor.c calls cb as (env, int64, int64, int64): a v : float slot would read a register it never loaded, so it is refused at compile time; a v : cstr slot compiles and runs on x86-64 / arm64, but the boxed callback's C type then differs from the one it is called through (a -fsanitize=function trap). int and ptr<void> slots are exact.

rreactor handle
chthe channel to watch (borrowed; an AsyncChan takes reactor-add-async-chan)
cbclosure: (fn [id :int value :int user :ptr<void>] :nil)
user-dataopaque value forwarded to cb

EventSourceId -- source id (>= 0), or -1 on error (unwrap with (:: id :int))

(reactor-add-chan r stop-chan
  (fn [id v user] :nil (reactor-stop r))
  nil)

Since: Phase R6

defn

reactor-add-async-chan

(reactor-add-async-chan [A])

reactor-add-chan for an AsyncChan.

The same one-shot watch over the same ChanBlock layout; only the handle type differs. The keep-alive rule and the carrier-word value are the same as reactor-add-chan's: do not async-chan-free the channel while the watch is live.

rreactor handle
chthe channel to watch (borrowed)
cbclosure: (fn [id :int value :int user :ptr<void>] :nil)
user-dataopaque value forwarded to cb

EventSourceId -- source id (>= 0), or -1 on error (unwrap with (:: id :int))

(reactor-add-async-chan r jobs
  (fn [id job user] :nil (run-job job))
  nil)

Since: Phase R6

defn

reactor-poll

(reactor-poll [^borrow r :Reactor timeout-ms :int])

poll for events once, dispatching ready callbacks.

Blocks up to timeout-ms milliseconds waiting for events. Pass -1 to block indefinitely; pass 0 for a non-blocking check. Returns after all callbacks for this batch have been dispatched.

rreactor handle
timeout-msmaximum wait in milliseconds (-1 = forever, 0 = non-blocking)

int -- number of callbacks dispatched (>= 0), or -1 on error

(reactor-poll r 100)  ; => 0..n

Since: Phase R1

defn

reactor-run

(reactor-run [^borrow r :Reactor])

run the event loop until stopped or all sources removed.

Calls reactor-poll in a loop until reactor-stop is called from a callback or another thread, or until no registered sources remain.

rreactor handle
(reactor-run r)

Since: Phase R1

defn

reactor-stop

(reactor-stop [^borrow r :Reactor])

ask reactor-run to return after the current dispatch round.

Thread-safe: safe to call from a callback inside reactor-run or from another OS thread. reactor-run will return once the current batch of callbacks is complete.

rreactor handle
(reactor-stop r)

Since: Phase R1

defn

reactor-wake

(reactor-wake [^borrow r :Reactor])

interrupt a reactor blocked in reactor-poll or reactor-run.

Thread-safe: causes the blocking poll to return immediately so the calling thread can check external state (e.g. a new fd to register) before re-entering the loop.

rreactor handle
(reactor-wake r)

Since: Phase R1

defopaque linear

LocalFiberGroup

(defopaque LocalFiberGroup :ptr<void> :linear)

a linear handle to a fiber group bound to one reactor.

defn

local-fiber-group-new

(local-fiber-group-new [^borrow r :Reactor])

create a fiber group bound to a reactor.

The group borrows the reactor; do not free the reactor while the group is alive. One group is bound to one reactor for its lifetime and is not thread-safe (same rule as the reactor itself).

rreactor handle returned by reactor-new

LocalFiberGroup -- the group handle; null only when r is (reactor-new's WASM case) or allocation fails

(let [r (reactor-new)
      g (local-fiber-group-new r)]
  (reactor-run-fibers g)
  (local-fiber-group-free g)
  (reactor-free r))

Since: Phase F2

defn

local-fiber-group-free

(local-fiber-group-free [g :LocalFiberGroup])

free a fiber group and cancel any parked fibers.

Any fiber still parked is cancelled: its park source is removed from the bound reactor and its stack is freed without unwinding (no cleanup hooks). The bound reactor is not freed.

ggroup handle returned by local-fiber-group-new
(local-fiber-group-free g)

Since: Phase F2

defn

local-spawn

(local-spawn [^borrow g :LocalFiberGroup ^fat body :(fn [ptr<void>] nil) user-data :ptr<void>])

spawn a fiber into a group.

The fiber first runs on the next reactor-run-fibers tick. The body is a closure taking the user-data pointer; its return value is ignored.

ggroup handle
bodyclosure: (fn [user :ptr<void>] :nil)
user-dataopaque value forwarded to body as its only argument

int -- fiber id (>= 0), or -1 on error

(local-spawn g
  (fn [user] :nil (println "hello from a fiber"))
  nil)

Since: Phase F3

defn

reactor-run-fibers

(reactor-run-fibers [^borrow g :LocalFiberGroup])

drive the reactor and pump the group's fibers.

Runs until either the group is empty and the bound reactor has no remaining sources, or reactor-stop is called on the bound reactor. Resumed fibers run until they park or return; parked fibers wake when their reactor source fires.

Calling this re-entrantly from inside a fiber of the same group is a hard error and returns -1 immediately (it would deadlock the pump).

ggroup handle

int -- number of fibers that ran to completion, or -1 on re-entry/error

(local-spawn g (fn [u] :nil (println "hi")) nil)
(reactor-run-fibers g)  ; => 1

Since: Phase F3

defn

local-park-fd

(local-park-fd [^borrow g :LocalFiberGroup fd :int events :int timeout-ms :int])

park the running fiber until an fd is ready.

Registers fd on the bound reactor for events (a READ/WRITE bitmask) and suspends the current fiber. The fiber resumes when the reactor fires the source; the source is removed before it runs again (one-shot). If timeout-ms is >= 0 and elapses first, the fiber resumes with -2.

Must be called from inside a fiber spawned into g. Outside of one it returns -1 and leaves the caller running.

ggroup handle
fdfile descriptor to wait on
eventsevent bitmask (READ | WRITE)
timeout-mstimeout in milliseconds, or -1 to wait forever

int -- the fired event mask (>= 0), -2 on timeout, or -1 if not in a fiber

(local-park-fd g read-fd READ -1)  ; => 1 when readable

Since: Phase F4

defn

local-park-chan

(local-park-chan [A])

park the running fiber until a channel has a value.

Registers ch on the bound reactor (one-shot) and suspends the current fiber. The fiber resumes with the received value when one is available. As with reactor-add-chan, prompt same-thread delivery requires the sender to call reactor-wake after chan-send (the driver otherwise blocks in poll).

The channel is borrowed, so the caller keeps the handle; keep it alive until the fiber has resumed. Must be called from inside a fiber spawned into g. Outside of one it returns (none) and leaves the caller running.

ggroup handle
chthe channel to park on (borrowed; an AsyncChan takes local-park-async-chan)

(Option A) -- (some v) with the received value, or (none) if not in a group fiber

(match (local-park-chan g ch)
  (Some v) (handle v)
  (None)   (not-in-a-fiber))

Since: Phase F5

defn

local-park-async-chan

(local-park-async-chan [A])

local-park-chan for an AsyncChan.

The same park over the same ChanBlock layout; only the handle type differs.

ggroup handle
chthe channel to park on (borrowed)

(Option A) -- (some v) with the received value, or (none) if not in a group fiber

(local-park-async-chan g jobs)  ; => (some job)

Since: Phase F5

Internal definitions