xtc_xproc(3)

---

xtc_xproc(3)

cross-fork spawn, send, and monitor

XTC_XPROC(3) Library Functions Manual XTC_XPROC(3)

xtc_xspawn, xtc_xproc_register_entry, xtc_xspawn_entry, xtc_xproc_destroy, xtc_xproc_os_pid, xtc_xsend, xtc_xmonitor, xtc_xlink, xtc_xproc_child_main, xtc_xproc_win_child_maybe — cross-fork spawn, send, and monitor

library “libxtc”

#include <xtc_xproc.h>

int
xtc_xspawn(xtc_loop_t *loop, const char *name, xtc_xproc_root_fn root_fn, const void *arg, size_t arg_len, xtc_xproc_t **out);

int
xtc_xproc_register_entry(const char *name, xtc_xproc_root_fn fn);

int
xtc_xspawn_entry(xtc_loop_t *loop, const char *name, const char *entry, const void *arg, size_t arg_len, xtc_xproc_t **out);

void
xtc_xproc_destroy(xtc_xproc_t *p);

long
xtc_xproc_os_pid(const xtc_xproc_t *p);

int
xtc_xsend(xtc_xproc_t *p, const void *msg, size_t len);

int
xtc_xmonitor(xtc_xproc_t *p, uint64_t *out_ref);

int
xtc_xlink(xtc_xproc_t *p);

int
xtc_xproc_child_main(int ctrl_fd, xtc_xproc_root_fn root_fn, void *arg);

int
xtc_xproc_win_child_maybe(int argc, char **argv);

These extend the Erlang-style process relations across a fork(2) boundary: a parent spawns a child that runs its OWN xtc runtime, and can send to it and monitor it as if it were a local process. This is the single-host subset of the (unbuilt) distributed design -- a stepping stone toward cluster support, built over a local socketpair rather than TCP.

() forks a child running root_fn, which receives a private copy of the arg / arg_len bytes the parent passed. The parent gets an xtc_xproc_t handle in *out. It must be called from a fiber on loop (the monitor relay lives there). On a platform without fork(2) it returns XTC_E_NOSYS, because a raw function pointer does not survive process creation there (a Windows child is a fresh image, not a clone of the parent) -- use xtc_xspawn_entry() there, and portably.

() registers a child root function under a NAME in a process-global table. The same binary -- parent and any re-exec'd child -- resolves the name to the same function, which is the portable bridge for platforms where a function pointer cannot cross process creation. Call it once, early, in code that runs in BOTH the parent and the child image. xtc_xspawn_entry() spawns a child running the function registered under entry by re-executing this binary, which resolves the name in its own copy of the registry (fork(2) and execve(2) on POSIX, () on Windows). The fresh image inherits no lock state from the parent, so this is the form to use from a multithreaded parent. It requires xtc_xproc_win_child_maybe() as the first statement of main() (see below); on POSIX, a binary that never calls it falls back to a plain fork, with the caveat described for xtc_xspawn(). It is otherwise identical to xtc_xspawn() (monitor, send, destroy all behave the same) and returns XTC_E_NOTFOUND if entry is not registered. This is the portable and Windows-capable spawn form.

() sends a byte payload (copied) to the child's root proc; it arrives in that proc's mailbox as an ordinary () message.

() monitors the child from the calling fiber. When the child exits, crashes, or its control channel dies, the caller receives a normal xtc DOWN message (decode with ()). The DOWN's reason carries the child's exit code, or the signal number if the child was killed by a signal, or XTC_DOWN_KIND_NOCONNECTION if the channel died before a clean exit was seen. Internally the child's fate is mirrored into a local shadow proc, so all the ordinary monitor/DOWN machinery applies unchanged.

() returns the child's OS pid (for logging), or -1. () terminates and reaps the child if it is still running, closes the channel, and frees the handle. A running child is sent SIGTERM, given up to 2 seconds to exit, then sent SIGKILL; either way it is reaped before destroy returns, so no orphan and no zombie remain. Call it from a process: the wait yields rather than blocking the loop. Passing NULL is a no-op; destroying the same handle twice is undefined.

The DOWN a monitor receives classifies the child's death exactly: a child that returns or exits with 0 is XTC_DOWN_KIND_CLEAN; one that exits with a nonzero code is XTC_DOWN_KIND_EXIT with that code; one killed by a signal is XTC_DOWN_KIND_SIGNAL with the signal number. So a child killed by SIGSEGV and one that called (11) are distinguishable. Releases before 1.50 reported every signal death as XTC_DOWN_KIND_EXIT with the signal number as the exit code, and every short-lived child's status as 96 (a truncated internal NOPROC code).

Destroying a monitored or linked child is safe: the monitor still receives its DOWN, because the internal shadow process that reports the child's fate holds its own reference to the child's state and releases it only when it is done.

Releases before 1.50 neither signalled nor reaped a running child (it was orphaned and later became a zombie), and freed state that a live monitor was still reading.

() is the child-side entry that xtc_xspawn() arranges automatically; it is public so a re-exec'd child can re-enter its runtime.

() is wired as the first statement of (), after the xtc_xproc_register_entry() calls, in a binary that hosts xtc_xspawn_entry() children, on every platform (the name predates POSIX re-exec). If the cross-process-child sentinel argv is present it connects the control channel, receives its arg, runs the registered entry, and exits without returning; otherwise it returns 0 and normal startup continues. On POSIX it also records that the binary has the hook, which is what enables re-exec: without it a re-exec'd child would run main() from the top and repeat whatever the parent did.

Releases before 1.50 forked the POSIX () child straight into a new runtime without exec, so a child forked while another parent thread held one of the library's process-wide locks could block forever; the hook was a no-op on POSIX then, so binaries that followed the earlier advice already have it.

() still forks without exec, because a raw function pointer is only meaningful in a clone of the parent. Its child runs a full runtime in a copy of a possibly multithreaded parent; prefer xtc_xspawn_entry() there.

xtc_xspawn(), xtc_xsend(), and xtc_xmonitor() return XTC_OK or a negative XTC_E_* code (notably XTC_E_NOSYS where fork(2) is unavailable). xtc_xproc_os_pid() returns the OS pid or -1. xtc_xproc_child_main() returns the child's exit code.

xtc_xproc_destroy() returns no value.

POSIX only; the Windows build declines every entry point with XTC_E_NOSYS.

Because cross-fork spawn uses a real fork(2) and waitpid(2), it is deliberately NOT reachable by the deterministic simulator (the sim is single-process and cannot observe a real child's schedule) -- the same boundary that keeps native AIO and the thread pool out of the sim. It is covered by the unit test tier (test/m10/test_xproc.c); a future DST model would run both sides as loops over a simulated socketpair, as the distributed design describes.

xtc_osproc(3), xtc_proc(3), xtc_net(3), xtc(7)

July 10, 2026 Debian

View the mdoc source