hosted/events
skip to the library navigationHosted targets only.
The hosted provider whose waiting operations switch fibres, written once over platform/posix, platform/linux's epoll and signalfd and platform/kqueue: the provider host, made by new, imported as events with no alias. A program that serves several things at once, or waits for a signal, mints it instead of blocking.new().
| Call | What it does |
|---|---|
events.new(space, entries) | Fill an events.space the caller declares zeroed and return a host over it and over entries, the table of fibres that may be unfinished at once, one more than a group's count. |
events.last_errno(host) | The errno number of the last failing call, in the process's cell. |
events.last_signal(host) | The signal that ended the last child waited for. |
host supplies every concept hosted/blocking does, world, programs, network, clock and the system's, and is a fibre.turns and a signal.signals beside them. Its fibres take turns on a stacks.round over the caller's table; when every fibre has taken a turn since anything last started or ended, the round waits for events: epoll on Linux and kqueue on Darwin and FreeBSD, limited by the earliest deadline any fibre waits for, and not at all when a fibre yielded of its own accord. With nothing waiting it returns at once.
- A call that would wait submits a record on its fibre's own stack and takes turns until the loop answers it: the descriptor is ready, the deadline passed, or what it waited on was closed. A completion interface could answer the same records with the call's result.
- The provider's sockets and a child's pipes are non-blocking. A file it did not make is polled first and read or written once it is ready, so a regular file is read at once, and a terminal or a pipe a fibre waits for lets the others run.
- Closing a file or a socket, or shutting a socket's side, through the provider wakes every fibre waiting on it, whose call fails
io_failed. - A socket's limits from
set_timeoutsbound each wait, and their passing istimed_out. A connect has no limit. sleepwaits for its deadline in the loop, never early: no timer or descriptor is made for it.- A child's end is waited for on a pidfd on Linux and with kqueue's process filter on Darwin and FreeBSD, then reaped.
watchblocks the signal and reads it through a signalfd on Linux, and ignores it and reports it through kqueue on Darwin and FreeBSD, SIGCHLD keeping its default there; every child started afterwards gets the signal's default back.stop_watchingreads what arrived, gives each signal its default back and wakes every fibre innext_signal.- The event queue is closed on exec and stays open until the process ends.
The entries that may switch are the table's above, under "Which calls may switch".
Items
public host: type = events_value
The hosted provider whose waiting operations switch fibres: io.world, io.programs, net.network, the monotonic time.clock, fibre.scheduler and fibre.turns, and signal.signals, each of whose waits lets the provider's other fibres run, and every concept hosted/system supplies, which it hands the system's handle it is built over: io.directories, io.environment, io.file_system, io.terminals, io.entropy and time.wall_clock. A handle: copies share one round and one loop. Its files are the system's, as hosted/system's descriptor_of, file_of and socket_of give them. Its entries that may switch are docs/library.md's list (D446).
public space: type = struct
round: stacks.round_space
events: loop_state
idling: idle_value
end space
Caller-owned state of an event provider: the round of turns its fibres take, and its event loop. Declare it zeroed and give it to new, which fills it; it must outlive the handle.
public last_errno: (handle: host) -> (number: i32)
Return errno captured for the terminal failing provider call, as hosted/system's last_errno does: the cell is the process's, which this provider's own calls report through too.
public last_signal: (handle: host) -> (number: i32)
Return the number of the signal that ended the child this handle last saw end through io.wait, as hosted/system's last_signal does.
public new: (inout state: space, entries: []mut usize)
-> (handle: host from state, entries)
! mem.out_of_memory | io.io_failed
Fill state and return a handle to the event provider over it and over entries: a round of turns over that table of the fibres that may be unfinished at once, one more than a group's count, an event queue, closed on exec, and the process's hosted/system handle, whose last-error and last-signal cells it reports through. Fails mem.out_of_memory when entries holds fewer than two, and io.io_failed when the system makes no queue, with its errno in the process's cell. Calling this before the hosted entry has established the process's arguments traps, as hosted/system's new does. The queue stays open until the process ends.
Example program
The program runs a server fibre and a client fibre over one loopback connection; each waits in receive while the other runs, so their lines interleave.
This complete program is one of the compiler's runtime tests. View source.
-- D485: two fibres exchanging over a loopback TCP connection -- under hosted/events. The server accepts and the client connects; each -- message is answered before the next is sent, so their lines can only -- interleave as the provider lets one fibre run while the other waits in -- `receive`. The home stack joins the group, waiting in the loop. import core/failure import core/fibre import core/io import core/net import hosted/events mut trace: [64]u8 = zeroed mut marks: usize = 0 -- Note one step: a letter and a digit, after a space from the last. mark: (letter: u8, digit: usize) -> none = if marks > 0 then trace[marks] = u8(' ') inc marks end if trace[marks] = letter trace[marks + 1] = u8('0') + u8(digit) marks += 2 end mark serving: type = struct host: events.host listener: io.file end serving -- Accept one connection and answer three messages, each with its -- number doubled. serve: (inout state: serving) -> none ! failure.reason = connection: io.file = try net.accept(state.host, state.listener) mut buffer: [1]u8 = zeroed for round in usize(0)..<3 do got: usize = try net.receive(state.host, connection, buffer) fail io.io_failed when got <> 1 mark(u8('s'), usize(buffer[0])) buffer[0] = buffer[0] * 2 try net.send(state.host, connection, buffer) _ = round end for try net.close_socket(state.host, connection) end serve asking: type = struct host: events.host peer: net.address end asking -- Connect and send three messages, waiting for each answer. ask: (inout state: asking) -> none ! failure.reason = connection: io.file = try net.connect(state.host, state.peer) mut buffer: [1]u8 = zeroed for round in usize(1)..<4 do buffer[0] = u8(round) mark(u8('c'), round) try net.send(state.host, connection, buffer) got: usize = try net.receive(state.host, connection, buffer) fail io.io_failed when got <> 1 mark(u8('a'), usize(buffer[0])) end for got: usize = try net.receive(state.host, connection, buffer) fail io.io_failed when got <> 0 try net.close_socket(state.host, connection) end ask mut backing: [2 * 32768]u8 = zeroed run: (host: events.host) -> none ! failure.reason = listener: io.file = try net.listen(host, net.loopback(net.ipv4, 0)) peer: net.address = try net.local_address(host, listener) mut tasks := try fibre.turns_over(any(addr host), backing, 2) defer fibre.join(tasks) else none server: serving = (host: host, listener: listener) try fibre.admit(tasks, serve, server) client: asking = (host: host, peer: peer) try fibre.admit(tasks, ask, client) try fibre.join(tasks) try net.close_socket(host, listener) try io.write(host, io.out(host), trace[0..<marks]) try io.write(host, io.out(host), "\n") end run public main: () -> (code: i32) = code = 1 mut state: events.space = zeroed mut entries: [3]usize = zeroed host := events.new(state, entries) else (problem) _ = problem return end code = 2 run(host) else (problem) _ = problem return end code = 0 end main
$ refine compile main.ldn -o events-tcp-exchange && ./events-tcp-exchange c1 s1 a2 c2 s2 a4 c3 s3 a6