Systems Programming

Part of: Reef Language Reference Last reviewed on version: 0.9.2

This guide covers Reef's systems programming capabilities for building daemons, init systems, and other low-level programs. These features were built for the Hammerhead/Zygaena project.


Process Management (sys.process)

Fork

import sys.process

proc main()
    match process.process_fork()
        ForkResult_Child() =>
            println("child")
            process.exit_now(0)  // IMPORTANT: use exit_now in forked children
        end
        ForkResult_Parent(pid) =>
            match process.process_wait_status(pid)
                Result_Ok(st) =>
                    println("child finished, shell code ${st.shell_code()}")
                end
                Result_Err(e) =>
                    println("wait failed")
                end
            end match
        end
        ForkResult_Failed() =>
            println("fork failed")
        end
    end match
end main

Important: Always use process.exit_now() in forked children instead of returning normally. Normal process exit runs GC/Active-Object cleanup, which waits on threads that do not exist in the child — only the forking thread survives fork() — and the child hangs.

This requirement still stands even though the runtime is otherwise fork-safe. The collector installs pthread_atfork handlers, so in a forked child locks are reinitialized, allocation keeps working, and reef_heaps_gc() degrades to a permanent no-op rather than deadlocking. None of that covers exit-time cleanup: a child that returns normally from main() still hangs. Use exit_now(), or exec().

Waiting for Children

Since 0.11 every wait returns how the child ended as a value:

type ExitStatus = enum
    Exited(int)          // exit code
    Signaled(int, bool)  // signal, core
end ExitStatus

st.success() is true only for Exited(0). st.shell_code() is the number a POSIX shell puts in $?: the exit code, or 128 + signal. core is true when the kernel reports that a core file was produced; it is best effort (false where the platform cannot tell, see exit_status_core_supported()) and depends on ulimit -c, coreadm or core_pattern. Stopped and continued children are never reported.

A failed wait is a SysError: SysError_NoChild() for ECHILD (not our child, already reaped, or no children left), SysError_InvalidPid(pid) when a one-child wait is given pid <= 0 (rejected rather than passed to waitpid, where -1 means any child and 0 any child in the group), and SysError_Errno(n) for anything else.

import sys.process
import core.option as option

proc main()
    let pid = option.unwrap_or(process.process_spawn_shell("sleep 1"), 0)
    match process.process_try_wait_status(pid)
        Result_Ok(Option_None()) => println("still running") end
        Result_Ok(Option_Some(ExitStatus_Exited(code))) => println("exit ${code}") end
        Result_Ok(Option_Some(ExitStatus_Signaled(sig, core))) => println("signal ${sig}") end
        Result_Err(SysError_NoChild()) => println("not our child") end
        Result_Err(SysError_InvalidPid(p)) => println("bad pid ${p}") end
        Result_Err(SysError_Errno(n)) => println("wait failed: errno ${n}") end
    end match
end main

ProcessCapture (from process_run_capture) carries the same status.

Constructor patterns nest in match statements, as above: inner binders are bound and first-match order holds through the nesting. Not yet in a match expression, or together with a when guard in the same match; the compiler says so.

Wait guarantees. The runtime never calls waitpid(-1, ...) on its own behalf: the only waits for "any child" are process_wait_any_status and process_wait_any_nohang_status (and the deprecated process_wait_any*), which you call explicitly. It never installs a SIGCHLD handler or changes SIGCHLD's disposition, so a program that reaps its own children (a supervisor) is never raced by the runtime. Blocking waits retry EINTR, which the GC's stop-the-world signal causes. reef-runtime/tests/check_waitpid_sites.sh guards these in the test suite.

Deprecated in 0.11, removed in 0.12: process_wait(pid): int (now returns shell_code(), or -1 only when the wait failed; a signal death used to return -1), process_try_wait, process_wait_any, process_wait_any_nohang, WaitAnyResult, the last-wait getters process_exit_code, process_exited_normally, process_was_signaled, process_term_signal, and ProcessCapture.exit_code (= shell_code()).

Spawn with Arguments

import sys.process
import core.option as option

proc main()
    let args = ["hello", "world"]
    match process.process_spawn("/bin/echo", args)
        Option_Some(pid) =>
            let code = process.process_wait(pid)
        end
        Option_None() =>
            println("spawn failed")
        end
    end match
end main

Exec (Replace Process)

import sys.process

proc main()
    match process.process_fork()
        ForkResult_Child() =>
            let args = ["-la", "/tmp"]
            process.process_exec("/bin/ls", args)
            // Only reached if exec fails
            process.exit_now(1)
        end
        ForkResult_Parent(pid) =>
            let code = process.process_wait(pid)
        end
        ForkResult_Failed() =>
            println("fork failed")
        end
    end match
end main

Session and Process Groups

import sys.process
import core.option as option

proc main()
    match process.process_fork()
        ForkResult_Child() =>
            // Create new session (detach from controlling terminal).
            // None means setsid failed -- the usual cause is already being
            // a process-group leader.
            let sid = option.unwrap_or(process.process_setsid(), 0 - 1)
            // Set file creation mask
            process.umask(0o022)
            // ... daemon work ...
            process.exit_now(0)
        end
        ForkResult_Parent(pid) =>
            process.process_wait(pid)
        end
        ForkResult_Failed() =>
            println("fork failed")
        end
    end match
end main

Available Functions

Function Description
process_fork(): ForkResult Fork process. Parent(pid) / Child / Failed
process_spawn(program, argv): Option[int] Fork + exec with a full argv. None if the spawn failed
process_spawn_shell(command): Option[int] Fork + exec /bin/sh -c. None if the spawn failed
process_run(program, args): Option[int] Fork + exec, prepending program as argv[0]
process_exec(program, argv): int Replace process image (does not return on success)
process_wait_status(pid): Result[ExitStatus, SysError] Wait for one child (blocking)
process_try_wait_status(pid): Result[Option[ExitStatus], SysError] Non-blocking; Ok(None) while still running
process_wait_any_status(): Result[ChildExit, SysError] Wait for any child; ChildExit{pid, status}
process_wait_any_nohang_status(): Result[Option[ChildExit], SysError] Non-blocking any-child wait
process_wait(pid): int Deprecated (0.11): shell_code(), or -1 if the wait failed
process_wait_any(): Option[int] Deprecated (0.11): wait for any child, Some(pid)
process_wait_any_nohang(): WaitAnyResult Deprecated (0.11): Exited(pid) / NoneReady / Failed
process_kill(pid, signum): bool Send signal to process
process_setsid(): Option[int] Create new session; Some(sid) on success
process_getpgid(pid): Option[int] Get process group ID
process_setpgid(pid, pgid): bool Set process group ID; true on success
killpg(pgrp, sig): bool Send signal to process group; true on success
umask(mask): int Set file creation mask, returns previous
exit_now(code) Immediate exit without cleanup (for forked children)
getpid(): int Get current process ID
getppid(): int Get parent process ID

File Descriptor Operations (sys.fd)

Pipes

import sys.fd
import core.option as option

proc main()
    // fd_pipe returns Option: None means pipe(2) FAILED (EMFILE/ENFILE).
    // There is deliberately no fd_pipe_or -- a defaulted pair would hand back
    // descriptors nobody opened.
    let pr = fd.fd_pipe()
    if option.is_none(pr)
        println("pipe failed")
        return
    end if
    let fds = option.unwrap(pr)
    let read_fd = fds[0]
    let write_fd = fds[1]

    fd.fd_write(write_fd, "hello pipe")
    fd.fd_close(write_fd)

    let data = fd.fd_read(read_fd, 256)
    println("read: ${data}")
    fd.fd_close(read_fd)
end main

File I/O

import sys.fd

proc main()
    // Open for writing (create + truncate)
    let flags = fd.O_WRONLY() + fd.O_CREAT() + fd.O_TRUNC()
    let wfd = fd.fd_open("/tmp/test.txt", flags, 0o644)
    fd.fd_write(wfd, "hello file")
    fd.fd_close(wfd)

    // Open for reading
    let rfd = fd.fd_open("/tmp/test.txt", fd.O_RDONLY(), 0)
    let content = fd.fd_read(rfd, 256)
    println("file: ${content}")
    fd.fd_close(rfd)
end main

Redirect stdout

import sys.fd
import sys.process

proc main()
    match process.process_fork()
        ForkResult_Child() =>
            let flags = fd.O_WRONLY() + fd.O_CREAT() + fd.O_TRUNC()
            let logfd = fd.fd_open("/tmp/output.log", flags, 0o644)
            fd.fd_dup2(logfd, fd.STDOUT())  // redirect stdout to file
            fd.fd_close(logfd)
            println("this goes to the log file")
            process.exit_now(0)
        end
        ForkResult_Parent(pid) =>
            process.process_wait(pid)
        end
        ForkResult_Failed() =>
            println("fork failed")
        end
    end match
end main

Available Functions

Function Description
fd_open(path, flags, mode): int Open file, returns fd
fd_close(fd): int Close file descriptor
fd_read(fd, max_len): string Read up to max_len bytes, returns string
fd_write(fd, data): int Write string data, returns bytes written
fd_dup(fd): int Duplicate file descriptor
fd_dup2(oldfd, newfd): int Duplicate to specific fd number
fd_pipe(): Option[[int]] Create pipe; Some([read_fd, write_fd]), None if pipe(2) failed
fd_set_nonblocking(fd, enable): int Set O_NONBLOCK flag
fd_set_cloexec(fd, enable): int Set FD_CLOEXEC flag

Constants: STDIN(), STDOUT(), STDERR(), O_RDONLY(), O_WRONLY(), O_RDWR(), O_CREAT(), O_TRUNC(), O_APPEND(), O_CLOEXEC()


Signal Handling (sys.signal)

Blocking and Waiting for Signals

import sys.signal
import sys.process

proc main()
    match process.process_fork()
        ForkResult_Child() =>
            // Block SIGTERM
            signal.signal_block(signal.SIGTERM())

            // Send SIGTERM to self — does not kill because blocked
            process.process_kill(process.getpid(), signal.SIGTERM())

            // Consume the pending signal
            let signals = [signal.SIGTERM()]
            let got = signal.signal_wait(signals)
            println("got signal: ${got}")

            process.exit_now(0)
        end
        ForkResult_Parent(child) =>
            process.process_wait(child)
        end
        ForkResult_Failed() =>
            println("fork failed")
        end
    end match
end main

Self-Pipe Pattern

The self-pipe pattern converts async signals into readable pipe events, making them safe to handle in an event loop:

import sys.signal
import sys.process
import sys.poll
import core.option as option

proc main()
    // Create self-pipe. Option: None means the syscall failed.
    let sp = signal.selfpipe_create()
    if option.is_none(sp)
        println("selfpipe failed")
        return
    end if
    let fds = option.unwrap(sp)
    let read_fd = fds[0]
    let write_fd = fds[1]

    // Register SIGALRM to write to the pipe
    signal.selfpipe_register(signal.SIGALRM(), write_fd)

    // Send signal
    process.process_kill(process.getpid(), signal.SIGALRM())

    // Can now poll for signals alongside other fds
    poll.poll_clear()
    poll.poll_add(read_fd, poll.POLLIN())
    let ready = poll.poll_wait(1000)

    if ready > 0
        let sig = signal.selfpipe_read(read_fd)
        println("received signal: ${sig}")
    end if
end main

Warning: SIGURG is reserved — the GC uses it to suspend threads when it stops the world. Installing your own SIGURG handler, blocking it, or registering it with the self-pipe would stall collection process-wide, so sys.signal refuses all three with EINVAL. See GC Signal Use.

Available Functions

Function Description
signal_block(signum): bool Block a signal
signal_unblock(signum): bool Unblock a signal
signal_wait(signals): int Wait for one of the specified signals
selfpipe_create(): Option[[int]] Create self-pipe; Some([read_fd, write_fd]), None on failure
selfpipe_register(signum, write_fd): bool Register signal for self-pipe delivery
selfpipe_read(read_fd): int Read signal number from self-pipe
SIGRTMIN(): int Get SIGRTMIN value
SIGRTMIN_offset(n): int Get SIGRTMIN + n

Unix Domain Sockets (net.unix)

import sys.process
import net.unix
import core.result as result

proc main()
    let path = "/tmp/reef_test.sock"

    match process.process_fork()
        ForkResult_Child() =>
            // Client: connect after delay
            mut i = 0
            while i < 10000000
                i = i + 1
            end while

            let conn = unix.unix_connect(path)
            if result.is_ok(conn)
                let fd = result.unwrap_ok(conn)
                let sent = unix.unix_send(fd, "hello")
                let reply = unix.unix_recv(fd, 256)
                if result.is_ok(reply)
                    println("client: ${result.unwrap_ok(reply)}")
                end if
                unix.unix_close(fd)
            end if
            process.exit_now(0)
        end
        ForkResult_Parent(pid) =>
            // Server: listen and accept
            let listener = unix.unix_listen(path, 5)
            if result.is_ok(listener)
                let server = result.unwrap_ok(listener)
                let accepted = unix.unix_accept(server)
                if result.is_ok(accepted)
                    let client = result.unwrap_ok(accepted)
                    let msg = unix.unix_recv(client, 256)
                    if result.is_ok(msg)
                        println("server: ${result.unwrap_ok(msg)}")
                    end if
                    let ack = unix.unix_send(client, "reply ok")
                    unix.unix_close(client)
                end if
                unix.unix_close(server)
                unix.unix_unlink(path)
            end if
            process.process_wait(pid)
        end
        ForkResult_Failed() =>
            println("fork failed")
        end
    end match
end main

Available Functions

Most of net.unix returns Result — there is no negative-fd sentinel to test against. Only unix_close and unix_unlink return a bare int.

Function Description
unix_connect(path): Result[int, Error] Connect to Unix socket; Ok carries the fd
unix_listen(path, backlog): Result[int, Error] Create listening socket; Ok carries the fd
unix_accept(server_fd): Result[int, Error] Accept connection; Ok carries the client fd
unix_send(fd, data): Result[int, Error] Send data; Ok carries the byte count
unix_recv(fd, max_len): Result[string, Error] Receive data
unix_close(fd): int Close socket
unix_unlink(path): int Remove socket file

Note that process.process_fork() does not return a Result — it returns ForkResult, a three-variant enum (Parent(pid), Child, Failed). Fork has no error value to carry, and its three outcomes are three different places to be, not one value plus an error.


Event Loop with poll(2) (sys.poll)

import sys.fd
import sys.poll
import core.option as option

proc main()
    let pr = fd.fd_pipe()
    if option.is_none(pr)
        println("pipe failed")
        return
    end if
    let fds = option.unwrap(pr)
    let read_fd = fds[0]
    let write_fd = fds[1]

    // Write data
    fd.fd_write(write_fd, "poll test")
    fd.fd_close(write_fd)

    // Poll for readability
    poll.poll_clear()
    let idx = poll.poll_add(read_fd, poll.POLLIN())
    let ready = poll.poll_wait(1000)  // 1 second timeout

    if ready > 0 and poll.poll_readable(idx)
        let data = fd.fd_read(read_fd, 64)
        println("read: ${data}")
    elif ready == 0
        println("timeout")
    end if

    fd.fd_close(read_fd)
end main

Available Functions

Function Description
poll_clear() Reset the poll fd set
poll_add(fd, events): int Add fd to poll set, returns index
poll_wait(timeout_ms): int Wait for events, returns count of ready fds
poll_count(): int Number of fds currently in this thread's poll set
poll_revents(index): Option[int] Raw revents for fd at index; None if the index is not in the set
poll_readable(index): bool Check if fd has POLLIN
poll_writable(index): bool Check if fd has POLLOUT
poll_error(index): bool Check if fd has POLLERR
poll_hangup(index): bool Check if fd has POLLHUP

The four boolean predicates report false for an index that is not in the set, which is indistinguishable from "that event did not occur". That is deliberate: they are closed-world predicates, not lookups, and the Result/Option adoption policy leaves bool predicates unchanged. poll_revents is the one that returns Option[int], because 0 there means "no pending events" -- a real answer that an out-of-range index used to be confused with. Use poll_count() if you need to validate an index you did not just get back from poll_add().

Constants: POLLIN(), POLLOUT(), POLLERR(), POLLHUP()

Limits: Up to 64 file descriptors per poll set (thread-local storage).


GC Configuration for Daemons

GC Signal Use

SIGURG is reserved for the Reef runtime. SIGUSR1 and SIGUSR2 are yours.

The collector stops the world preemptively, using SIGURG as the suspension channel. When a collection begins, the collector signals every other registered thread; each target's handler publishes that thread's own precise roots and then parks in sigsuspend until the world restarts. Mutator threads do not poll a request flag and do not run forward to a safepoint — the older cooperative design (a gc_requested flag plus gc_safepoint() acknowledgements) was removed from the runtime.

Only the mark phase stops the world. Sweeping is lazy and incremental: it happens inside the allocator, on later allocations, and contributes no pause.

For a daemon this has one hard consequence:

  • Do not block, handle, or otherwise capture SIGURG. Doing so prevents threads from reaching the suspension handler and the collector waits forever — a process-wide hang with no diagnostic.
  • The runtime enforces this rather than trusting convention. sys.signal refuses operations on SIGURG — blocking it, installing a handler, or registering it with the self-pipe all fail with EINVAL instead of quietly breaking collection.

The GC uses no other signal. SIGUSR1/SIGUSR2 are unused by the runtime and free for application use.

Disabling GC Entirely

reefc simple.reef --no-gc

Skips reef_heaps_init, reef_objects_init, and related cleanup. Only use for programs that:

  • Don't use Active Objects
  • Don't use string interpolation ("${expr}" requires heap)
  • Don't allocate heap objects (arrays, structs created with new)

reef_machine_init/cleanup is always kept (provides TLS and platform setup).


Complete Daemon Example

import sys.process
import sys.signal
import sys.fd
import sys.poll
import net.unix
import core.result as result
import core.option as option

proc main()
    match process.process_fork()
        ForkResult_Child() =>
            // Detach from terminal
            process.process_setsid()
            process.umask(0o022)

            // Set up self-pipe for signal handling
            let sp = signal.selfpipe_create()
            if option.is_none(sp)
                process.exit_now(1)
            end if
            let sigfds = option.unwrap(sp)
            signal.selfpipe_register(signal.SIGTERM(), sigfds[1])

            // Create control socket. unix_listen returns Result — bail out if
            // the socket could not be created rather than polling a bad fd.
            let listener = unix.unix_listen("/var/run/myapp.sock", 5)
            if result.is_err(listener)
                process.exit_now(1)
            end if
            let server = result.unwrap_ok(listener)

            // Event loop
            mut running = true
            while running
                poll.poll_clear()
                let sig_idx = poll.poll_add(sigfds[0], poll.POLLIN())
                let srv_idx = poll.poll_add(server, poll.POLLIN())
                let ready = poll.poll_wait(5000)

                if ready > 0
                    if poll.poll_readable(sig_idx)
                        let sig = signal.selfpipe_read(sigfds[0])
                        if sig == signal.SIGTERM()
                            running = false
                        end if
                    end if
                    if poll.poll_readable(srv_idx)
                        let accepted = unix.unix_accept(server)
                        if result.is_ok(accepted)
                            let client = result.unwrap_ok(accepted)
                            let cmd = unix.unix_recv(client, 256)
                            let ack = unix.unix_send(client, "ok")
                            unix.unix_close(client)
                        end if
                    end if
                end if
            end while

            unix.unix_close(server)
            unix.unix_unlink("/var/run/myapp.sock")
            process.exit_now(0)
        end
        ForkResult_Parent(pid) =>
            println("daemon started: pid=${pid}")
        end
        ForkResult_Failed() =>
            println("fork failed")
        end
    end match
end main

Compile for daemon use:

reefc daemon.reef -o myapp