xtc_proc(3)
---xtc_proc(3)
BEAM-style lightweight processes with mailboxes
| XTC_PROC(3) | Library Functions Manual | XTC_PROC(3) |
NAME
xtc_proc_spawn,
xtc_proc_spawn_link,
xtc_proc_spawn_monitor,
xtc_self, xtc_send,
xtc_recv, xtc_recv_match,
xtc_recv_correlate,
xtc_proc_wait_fd,
xtc_proc_sleep,
xtc_exit_self, xtc_exit_pid,
xtc_link, xtc_unlink,
xtc_monitor,
xtc_proc_mailbox_stats,
xtc_fault_guard_install,
xtc_proc_recovery_arm,
xtc_proc_critical_enter,
xtc_proc_at_exit,
xtc_down_decode
xtc_down_decode_ex —
BEAM-style lightweight processes with mailboxes
SYNOPSIS
#include <xtc.h>
#include <xtc_proc.h>
int
xtc_proc_spawn(xtc_loop_t
*loop, xtc_proc_fn
fn, void *arg,
const xtc_proc_opts_t
*opts, xtc_pid_t
*out_pid);
int
xtc_proc_spawn_link(xtc_loop_t
*loop, xtc_proc_fn
fn, void *arg,
const xtc_proc_opts_t
*opts, xtc_pid_t
*out_pid);
int
xtc_proc_spawn_monitor(xtc_loop_t
*loop, xtc_proc_fn
fn, void *arg,
const xtc_proc_opts_t
*opts, xtc_pid_t
*out_pid, uint64_t
*out_ref);
xtc_pid_t
xtc_self(void);
int
xtc_send(xtc_pid_t
target, const void
*data, size_t
size);
int
xtc_recv(void
**out, size_t
*out_size, int64_t
timeout_ns);
int
xtc_recv_match(xtc_match_fn
match_fn, void
*user_data, void
**out, size_t
*out_size, int64_t
timeout_ns);
int
xtc_recv_correlate(const
void *corr_value, size_t
corr_size, int
n_expected, xtc_msg_t
*out_msgs, int
*out_n, int64_t
timeout_ns);
int
xtc_proc_wait_fd(int
fd, uint32_t
interest, int64_t
timeout_ns, uint32_t
*out_revents);
int
xtc_proc_sleep(int64_t
ns);
int
xtc_exit_self(int
reason);
int
xtc_exit_pid(xtc_pid_t
pid, int
reason);
int
xtc_link(xtc_pid_t
other);
int
xtc_unlink(xtc_pid_t
other);
int
xtc_monitor(xtc_pid_t
target, uint64_t
*out_ref);
int
xtc_proc_mailbox_stats(xtc_pid_t
pid, xtc_mailbox_stats_t
*out);
int
xtc_fault_guard_install(void);
int
xtc_proc_recovery_arm(void);
void
xtc_proc_recovery_disarm(void);
void
xtc_proc_critical_enter(void);
void
xtc_proc_critical_leave(void);
int
xtc_proc_at_exit(void
(*fn)(void *), void
*arg);
struct xtc_mctx *
xtc_proc_mctx(void);
int
xtc_down_decode(const
void *msg, size_t
len, xtc_pid_t
*out_pid, int
*out_reason);
int
xtc_down_decode_ex(const
void *msg, size_t
len, xtc_down_info_t
*out);
DESCRIPTION
xtc processes are BEAM-style lightweight actors: each has an identity (xtc_pid_t), a private mailbox, and a stack-backed coroutine that runs co-operatively inside an xtc_loop(3). Communication is by message passing. Failures are isolated by xtc_supervisor(3) or by manual link/monitor.
xtc_proc_spawn()
creates a new process running fn
(arg). The process runs on loop
co-operatively with all other procs and tasks on that loop.
opts is optional and may set the mailbox capacity,
initial stack size, or per-proc resource accountant.
A process is pinned to loop for its lifetime: it runs only on that loop and is never migrated by work-stealing. This affinity is guaranteed and requires no opt-in -- only raw xtc_exec(3) tasks are stealable. Embedders that pin a worker to one loop (e.g. a PostgreSQL backend per loop) can rely on it.
xtc_proc_spawn_link()
and
xtc_proc_spawn_monitor()
are the atomic spawn-and-relate primitives, mirroring Erlang's
spawn_link()
and
spawn_monitor().
They behave exactly like xtc_proc_spawn(), except
that the parent<->child relationship is established BEFORE the child
is made runnable, so there is no window in which the child exists but is not
yet linked or monitored. Even a child that runs and exits immediately
therefore delivers its ‘EXIT’ (link) or ‘DOWN’
(monitor) to the parent, never the XTC_DOWN_NOPROC
"monitor raced a dead target" outcome that a
xtc_proc_spawn() followed by a separate
xtc_link() or xtc_monitor()
can hit. xtc_proc_spawn_link() establishes a
bidirectional link (as if xtc_link() were called on
the child the instant it is born);
xtc_proc_spawn_monitor() establishes a
unidirectional monitor and returns its reference in
out_ref. Both require the CALLER to be a process
(xtc_self() !=
XTC_PID_NONE) and return
XTC_E_INVAL otherwise.
xtc_self()
xtc_send()
appends an envelope holding a copy of data
[0 .. size) to the mailbox of
target. Returns XTC_OK on
success, XTC_E_INVAL if the target is dead, or
XTC_E_AGAIN if the target's mailbox is full and
bounded. Sending is asynchronous: the send returns immediately whether or
not the receiver is currently scheduled.
Mailboxes are bounded by mailbox_cap
(default 4096) so that a fast sender cannot drive the receiver out of
memory. A caller that ignores an XTC_E_AGAIN return
silently loses the message. Senders therefore MUST handle it: retry later,
shed and account the load, apply end-to-end flow control, or treat it as
fatal on a must-deliver path.
xtc_recv()
delivers the next message from the calling proc's mailbox in arrival order.
If the mailbox is empty, timeout_ns controls
behaviour:
- timeout_ns < 0
- Block forever.
- timeout_ns == 0
- Return
XTC_E_AGAINimmediately if no message is queued. - timeout_ns > 0
- Block up to that many nanoseconds, then return
XTC_E_AGAINon timeout.
xtc_recv_match()
performs selective receive: the callback match_fn is
invoked for each envelope in arrival order; the first one for which it
returns 1 is delivered. Non-matching envelopes are parked in a save queue
and re-checked on the next call only if the predicate has changed
(BEAM-style recv-mark optimisation).
xtc_recv_correlate()
collects n_expected messages whose first
corr_size bytes match
corr_value. Useful for fork-join patterns where the
parent assigns each child a correlation id and waits for replies tagged with
that id. The collected messages are written to
out_msgs; *out_n is set to the
count actually delivered (which may be less than
n_expected if the timeout fires first).
xtc_proc_wait_fd()
blocks until any of:
- fd becomes ready in any
interest direction
(
XTC_IO_READABLE|XTC_IO_WRITABLE|XTC_IO_HUP|XTC_IO_ERR), - a message arrives in the calling proc's mailbox
(
XTC_WAIT_MAILBOX) - the timeout elapses (
XTC_WAIT_TIMEOUT) - the proc is killed via
xtc_exit_pid().
xtc_proc_sleep()
parks the calling proc on a timer for at least ns
nanoseconds. The loop runs other work meanwhile -- it does not block the
thread -- and, unlike a timed xtc_recv(), it does
not touch the mailbox. Returns XTC_E_INVAL if not
called from a proc.
xtc_exit_self()
terminates the calling process; cleanup runs and any links / monitors
fire.
xtc_exit_pid()
terminates another process by pid. Asynchronous; the target notices on its
next yield or xtc_recv() parking point.
xtc_link()
creates a bidirectional link. If either end exits with a non-normal reason,
the other receives an ‘EXIT’ signal in its mailbox.
xtc_unlink()
removes the link.
xtc_monitor()
creates a unidirectional monitor. The watcher receives a
‘DOWN’ message ({kind='D', ref, pid,
reason}) when the monitored process exits.
xtc_proc_mailbox_stats()
snapshots a process's mailbox into out: the current
depth, the selective-receive
saved count, the high-water
peak, the cap, and lifetime
recv_total and drop_total counts
(messages accepted and rejected). It is safe to call from any thread and is
the basis for an overload / head-of-line-buildup scrape.
The capacity bound applies to
depth +
saved, not the mailbox alone: a selective receiver
that moves non-matching messages aside still counts them, so a flood of
non-matching traffic cannot grow a process without limit by draining the
mailbox into the save queue. A consequence is that a process selectively
waiting for a message while its budget fills with non-matching ones will see
further sends rejected with XTC_E_AGAIN (including
the awaited one): the bound is a hard backpressure contract, so design
selective waits with that in mind (prefer
xtc_recv_correlate()
and bounded timeouts).
A process can also be given a mailbox watermark via
xtc_proc_opts_t.mailbox_watermark_pct (1..100; 0
disables) plus mailbox_watermark_fn and
mailbox_watermark_user. When an accepted message first
brings the depth to that percent of the cap, the callback fires once on the
rising edge -- so an application can shed load before the hard cap rejects
with XTC_E_AGAIN. The callback runs on the sender's
thread outside the mailbox lock and must not block.
xtc_fault_guard_install()
installs a process-wide handler for the synchronous, fiber-directed faults
SIGSEGV, SIGBUS,
SIGFPE and SIGILL on an
alternate signal stack.
Fault containment is PER-OS-THREAD:
xtc_fault_guard_install()
must have run on every OS thread that hosts a loop before a fiber fault on
that thread can be contained and turned into a DOWN. A common idiom is to
install it at the entry of one long-lived fiber per loop thread (a
supervisor); the call is idempotent, so repeated installs are harmless. A
loop thread on which the guard was never installed lets a fiber fault take
down the whole process rather than containing it.
xtc_proc_recovery_arm()
arms a recovery frame for the calling process (used like
sigsetjmp(3): it returns 0 normally and the fault signal
number when a contained fault returns control there). On such a fault the
handler identifies the faulting process -- it is the faulting thread's
running process -- and, if it armed a recovery frame and is not in a
critical section, unwinds only that fiber back to the frame; siblings on the
loop are untouched. The recovered process releases its resources and calls
xtc_exit_self(), so a supervisor observes the fault
as a normal ‘DOWN’.
xtc_proc_critical_enter()
and
xtc_proc_critical_leave()
bracket a region in which a fault must NOT be contained -- shared state may
be torn -- so it escalates to process abort, preserving the embedder's
critical-section panic semantics. The recovery frame only unwinds the call
stack; locks, fds, allocations and other resources held at fault time are
the recovery block's responsibility to release. Containment is POSIX-only;
on Windows the calls compile but a fault keeps its process-wide
disposition.
xtc_proc_at_exit()
registers a callback to run when the calling process exits -- on a normal
return or a contained-fault recovery -- LIFO, outside signal context, and
before the proc's monitors observe its ‘DOWN’. It is where an
embedder guarantees a faulted session releases what it held: register the
lock manager's release-all so no lock outlives the proc, close fds, or reset
a memory context.
xtc_proc_mctx()
returns a memory context scoped to the process, created on first use and
destroyed automatically at exit -- a backstop so a session's allocations are
reclaimed even if its recovery block faults.
xtc_down_decode()
extracts the target pid and exit reason from a ‘DOWN’ signal.
The DOWN and EXIT signals are sent PACKED; a consumer that casts the message
to its own unpacked mirror struct misreads the fields (the
reason in particular). Use this helper rather than
hand-rolling the layout.
RETURN VALUES
All entry points return XTC_OK on success.
Negative values are stable XTC_E_* codes; see
xtc_strerror(3).
EXAMPLES
A request-reply pattern with selective receive and timeouts:
static void server(void *arg) {
void *msg; size_t sz;
for (;;) {
if (xtc_recv(&msg, &sz, 5000LL * 1000 * 1000) != XTC_OK)
continue;
/* ... handle msg, reply via xtc_send to msg->reply_to ... */
free(msg);
}
}
xtc_pid_t pid;
xtc_proc_spawn(loop, server, NULL, NULL, &pid);
SEE ALSO
xtc_chan(3), xtc_loop(3), xtc_supervisor(3), xtc_svr(3), xtc_strerror(3), xtc(7)
HISTORY
xtc_proc_spawn appeared in xtc 0.1.
| May 28, 2026 | Debian |