xtc_proc(3)

---

xtc_proc(3)

BEAM-style lightweight processes with mailboxes

XTC_PROC(3) Library Functions Manual XTC_PROC(3)

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_exBEAM-style lightweight processes with mailboxes

#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);

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.

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

()

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

() 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_AGAIN immediately if no message is queued.
timeout_ns > 0
Block up to that many nanoseconds, then return XTC_E_AGAIN on timeout.
The delivered buffer is owned by the caller and must be free(3) 'd when no longer needed.

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

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

() 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().
*out_revents receives a bitmask of which conditions fired. This is the canonical way to write an idle-CPU-zero server: the proc sleeps until something real happens.

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

() terminates the calling process; cleanup runs and any links / monitors fire.

() terminates another process by pid. Asynchronous; the target notices on its next yield or xtc_recv() parking point.

() creates a unidirectional monitor. The watcher receives a ‘DOWN’ message ({kind='D', ref, pid, reason}) when the monitored process exits.

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

() 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: () 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. () 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’. () and () 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.

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

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

All entry points return XTC_OK on success. Negative values are stable XTC_E_* codes; see xtc_strerror(3).

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);

xtc_chan(3), xtc_loop(3), xtc_supervisor(3), xtc_svr(3), xtc_strerror(3), xtc(7)

xtc_proc_spawn appeared in xtc 0.1.

May 28, 2026 Debian

View the mdoc source