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.signalrefuses operations onSIGURG— blocking it, installing a handler, or registering it with the self-pipe all fail withEINVALinstead 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