Systems Programming

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

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()
    let pid = process.process_fork()
    if pid == 0
        // Child process
        println("child")
        process.exit_now(0)  // IMPORTANT: use exit_now in forked children
    end if
    if pid > 0
        // Parent process
        let exit_code = process.process_wait(pid)
        println("child exited: ${exit_code}")
    else
        println("fork failed")
    end if
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().

Spawn with Arguments

import sys.process

proc main()
    let args = ["hello", "world"]
    let pid = process.process_spawn("/bin/echo", args)
    if pid > 0
        let code = process.process_wait(pid)
    end if
end main

Exec (Replace Process)

import sys.process

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

Session and Process Groups

import sys.process

proc main()
    let pid = process.process_fork()
    if pid == 0
        // Create new session (detach from controlling terminal)
        let sid = process.process_setsid()
        // Set file creation mask
        process.umask(0o022)
        // ... daemon work ...
        process.exit_now(0)
    end if
    if pid > 0
        process.process_wait(pid)
    end if
end main

Available Functions

Function Description
process_fork(): int Fork process. Returns child PID in parent, 0 in child, -1 on error
process_spawn(program, args): int Fork + exec with arguments. Returns child PID
process_exec(program, args): int Replace process image (does not return on success)
process_wait(pid): int Wait for child to exit (blocking). Returns exit code
process_wait_any(): int Wait for any child. Returns PID of exited child
process_wait_any_nohang(): int Non-blocking wait. Returns PID, 0 if none ready
process_kill(pid, signum): bool Send signal to process
process_setsid(): int Create new session
process_getpgid(pid): int Get process group ID
process_setpgid(pid, pgid): int Set process group ID
killpg(pgrp, sig): int Send signal to process group
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

proc main()
    let fds = fd.fd_pipe()
    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()
    let pid = process.process_fork()
    if pid == 0
        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 if
    if pid > 0
        process.process_wait(pid)
    end if
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(): [int] Create pipe, returns [read_fd, write_fd]
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()
    let child = process.process_fork()
    if child == 0
        // 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 if
    if child > 0
        process.process_wait(child)
    end if
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

proc main()
    // Create self-pipe
    let fds = signal.selfpipe_create()
    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(): [int] Create self-pipe [read_fd, write_fd]
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"

    let pid = process.process_fork()
    if pid == 0
        // 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 if
    if pid > 0
        // 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 if
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 a plain int using the usual fork convention (0 in the child, the child's pid in the parent, negative on failure).


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

import sys.fd
import sys.poll

proc main()
    let fds = fd.fd_pipe()
    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_revents(index): int Get raw revents for fd at index
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

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

proc main()
    let pid = process.process_fork()
    if pid == 0
        // Detach from terminal
        process.process_setsid()
        process.umask(0o022)

        // Set up self-pipe for signal handling
        let sigfds = signal.selfpipe_create()
        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 if
    if pid > 0
        println("daemon started: pid=${pid}")
    end if
end main

Compile for daemon use:

reefc daemon.reef -o myapp