Landin library reference discuss source

Hosted 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_timeouts bound each wait, and their passing is timed_out. A connect has no limit.
  • sleep waits 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.
  • watch blocks 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_watching reads what arrived, gives each signal its default back and wakes every fibre in next_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

Types 2

Also on this page

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.

compiler/tests/fixtures/runtime/events-tcp-exchange/main.ldn100 lines
--  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
in a terminalprints on standard output
$ refine compile main.ldn -o events-tcp-exchange && ./events-tcp-exchange
c1 s1 a2 c2 s2 a4 c3 s3 a6