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.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
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