libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_proc.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License.
4 *
5 * src/inc/xtc_proc.h
6 * Lightweight processes with mailboxes, selective receive,
7 * links, monitors, and explicit exit. M8 ships the core; the
8 * `xtc_orc` supervisor (M10) sits on top of these primitives.
9 *
10 * A process is a coroutine with identity (xtc_pid_t) plus a
11 * mailbox. Send is fire-and-forget; the message is copied into
12 * an envelope owned by the mailbox. Receive is selective: the
13 * caller supplies a match function, and envelopes that don't
14 * match are kept in arrival order in a save queue and re-tested
15 * on the next receive.
16 */
17
18#ifndef XTC_PROC_H
19#define XTC_PROC_H
20
21#include "xtc_export.h"
22
23#include <stddef.h>
24#include <stdint.h>
25
26#include "xtc.h"
27#include "xtc_loop.h"
28#include "xtc_async.h"
29#include "xtc_exec.h" /* xtc_exec_class_t for xtc_proc_opts_t.sched_class */
30
31/*
32 * Process identifier. Encodes the loop ID, a per-loop slot index,
33 * and a generation counter so a stale pid (after a process exits and
34 * its slot is reused) is recognisably stale on lookup.
35 */
36typedef struct xtc_pid {
37 uint16_t loop_id;
38 uint16_t local_id;
39 uint32_t gen;
40} xtc_pid_t;
41
42#define XTC_PID_NONE ((xtc_pid_t){0, 0, 0})
43
44static inline int
45xtc_pid_eq(xtc_pid_t a, xtc_pid_t b)
46{
47 return a.loop_id == b.loop_id &&
48 a.local_id == b.local_id &&
49 a.gen == b.gen;
50}
51
52static inline int
53xtc_pid_is_none(xtc_pid_t p)
54{
55 return p.loop_id == 0 && p.local_id == 0 && p.gen == 0;
56}
57
58/*
59 * Match callback for selective receive. Inspects an envelope's
60 * data + size and returns:
61 * 1 -> consume this envelope (the receive call returns it)
62 * 0 -> skip; envelope stays in the save queue for the next receive
63 */
64typedef int (*xtc_match_fn)(const void *data, size_t size, void *user_data);
65
66/*
67 * Process entry function. The proc runs as a coroutine and the
68 * function returns nothing; exit happens by returning from the entry,
69 * by calling xtc_exit, or by being killed via a link.
70 */
71typedef void (*xtc_proc_fn)(void *arg);
72
73typedef struct xtc_proc_opts {
74 const char *name; /* optional, for debug */
75 size_t mailbox_cap; /* 0 = default */
76 int link_to; /* if != 0, this is a pid index to link to */
77 /* Mailbox watermark: when an accepted message brings the depth to
78 * this percent of mailbox_cap (1..100; 0 = disabled), the callback
79 * fires once on the rising edge, so the app can shed load before
80 * the hard cap rejects with XTC_E_AGAIN. The callback runs on the
81 * sender's thread, outside the mailbox lock; keep it cheap and do
82 * not block. */
83 int mailbox_watermark_pct;
84 void (*mailbox_watermark_fn)(xtc_pid_t self, size_t depth,
85 size_t cap, void *user);
86 void *mailbox_watermark_user;
87 /*
88 * Work-stealing: 0 (default) pins the proc's coroutine to its
89 * spawn loop -- today's behavior, and what a zeroed opts gives.
90 * 1 makes the coroutine work-stealable: an idle loop may steal it
91 * (only while parked/scheduled, never mid-instruction) to rebalance
92 * runnable work across a multi-loop executor. The proc's identity
93 * (pid), supervision (links/monitors/DOWN), recovery frame, and
94 * mailbox are unaffected -- only the carrier loop can change, and
95 * only at a yield point. See xtc_proc(3) MIGRATION.
96 */
97 int migratable;
98 /*
99 * L1 proportional-share scheduler: place this proc's task in a
100 * scheduling CLASS created with xtc_exec_class_create. NULL (the
101 * default, and what a zeroed opts gives) is the implicit plain-FIFO
102 * class -- zero overhead. A non-NULL handle weights the proc's CPU
103 * share against the other classes on its loop. INSPIRED BY Glommio.
104 */
105 xtc_exec_class_t sched_class;
107
108/* Mailbox statistics snapshot (see xtc_proc_mailbox_stats). */
109typedef struct xtc_mailbox_stats {
110 size_t depth; /* messages currently in the mailbox */
111 size_t saved; /* messages held in the selective-receive
112 * save queue (inspected, not yet matched) */
113 size_t peak; /* high-water mailbox depth ever reached */
114 size_t cap; /* capacity bound on depth + saved (0 = none) */
115 uint64_t recv_total; /* messages accepted over the proc's life */
116 uint64_t drop_total; /* messages rejected (full / dead) */
118
119/*
120 * PUBLIC: int xtc_proc_spawn __P((xtc_loop_t *, xtc_proc_fn, void *, const xtc_proc_opts_t *, xtc_pid_t *));
121 * PUBLIC: int xtc_proc_spawn_link __P((xtc_loop_t *, xtc_proc_fn, void *, const xtc_proc_opts_t *, xtc_pid_t *));
122 * PUBLIC: int xtc_proc_spawn_monitor __P((xtc_loop_t *, xtc_proc_fn, void *, const xtc_proc_opts_t *, xtc_pid_t *, uint64_t *));
123 * PUBLIC: xtc_pid_t xtc_self __P((void));
124 * PUBLIC: int xtc_proc_set_userdata __P((void *));
125 * PUBLIC: void *xtc_proc_userdata __P((void));
126 * PUBLIC: int xtc_proc_set_class __P((xtc_exec_class_t));
127 * PUBLIC: int xtc_send __P((xtc_pid_t, const void *, size_t));
128 * PUBLIC: int xtc_recv __P((void **, size_t *, int64_t));
129 * PUBLIC: int xtc_recv_match __P((xtc_match_fn, void *, void **, size_t *, int64_t));
130 * PUBLIC: int xtc_recv_correlate __P((const void *, size_t, int, xtc_msg_t *, int *, int64_t));
131 * PUBLIC: int xtc_proc_wait_fd __P((int, uint32_t, int64_t, uint32_t *));
132 * PUBLIC: int xtc_proc_sleep __P((int64_t));
133 * PUBLIC: int xtc_exit_self __P((int));
134 * PUBLIC: int xtc_exit_pid __P((xtc_pid_t, int));
135 * PUBLIC: int xtc_proc_wake __P((xtc_pid_t));
136 * PUBLIC: int xtc_link __P((xtc_pid_t));
137 * PUBLIC: int xtc_unlink __P((xtc_pid_t));
138 * PUBLIC: int xtc_monitor __P((xtc_pid_t, uint64_t *));
139 */
140
141XTC_API int xtc_proc_spawn(xtc_loop_t *loop, xtc_proc_fn fn, void *arg,
142 const xtc_proc_opts_t *opts, xtc_pid_t *out_pid);
143
144/*
145 * Atomic spawn + link / spawn + monitor (Erlang spawn_link /
146 * spawn_monitor). Identical to xtc_proc_spawn, but the parent<->child
147 * relationship is established BEFORE the child is made runnable, so
148 * there is no window in which the child exists but is not yet
149 * linked/monitored -- even if the child runs and exits immediately,
150 * its EXIT/DOWN is delivered (no XTC_DOWN_NOPROC race that a
151 * spawn-then-link/monitor idiom can hit). The CALLER MUST be a
152 * process (xtc_self() != NONE); returns XTC_E_INVAL otherwise.
153 *
154 * _link: bidirectional fate, exactly like calling xtc_link(child)
155 * the instant the child is born -- an abnormal exit on either
156 * side raises an EXIT on the other.
157 * _monitor: unidirectional; the caller receives a DOWN (with *out_ref
158 * as the monitor reference) when the child exits, exactly
159 * like xtc_monitor(child).
160 */
161XTC_API int xtc_proc_spawn_link(xtc_loop_t *loop, xtc_proc_fn fn, void *arg,
162 const xtc_proc_opts_t *opts, xtc_pid_t *out_pid);
163XTC_API int xtc_proc_spawn_monitor(xtc_loop_t *loop, xtc_proc_fn fn,
164 void *arg, const xtc_proc_opts_t *opts,
165 xtc_pid_t *out_pid, uint64_t *out_ref);
166
167/* From inside a process, return its pid; from outside, returns NONE. */
168/*
169 * Asynchronous cross-process exit signal. Sets a kill flag on the
170 * target proc; the target raises the exit at its next yield/recv
171 * point with the supplied reason. Idempotent (first call wins).
172 * Returns XTC_E_INVAL if the target is unknown or already dead.
173 */
174XTC_API int xtc_exit_pid(xtc_pid_t target, int reason);
175
176/*
177 * Resume a process parked in xtc_proc_wait_fd / xtc_recv, from ANY OS
178 * thread (including a thread libxtc does not manage). The explicit
179 * "poke the target loop" primitive: after a foreign thread makes a
180 * watched condition true (writes a self-pipe an fd-park watches, sets
181 * an embedder latch, completes an async read), xtc_proc_wake(pid)
182 * nudges the target loop out of its I/O wait so the parked proc
183 * re-checks its condition. It delivers no message; the woken proc just
184 * re-evaluates, so a spurious wake is always safe. Returns XTC_OK
185 * (including when the target is not parked, already runnable, or gone),
186 * XTC_E_INVAL for XTC_PID_NONE.
187 */
188XTC_API int xtc_proc_wake(xtc_pid_t target);
189
190XTC_API xtc_pid_t xtc_self(void);
191
192/*
193 * Per-proc opaque consumer pointer, on the CALLING proc.
194 *
195 * xtc_proc_set_userdata stores an opaque `void *` on the calling proc
196 * (NULL by default); xtc_proc_userdata reads it back. The pointer
197 * belongs to the proc, not the loop or OS thread, so it SURVIVES
198 * work-stealing migration -- a migratable proc
199 * (xtc_proc_opts_t.migratable) that resumes on a different loop still
200 * reads its own value. This is the supported way to associate
201 * per-proc consumer state that must track the running proc across a
202 * carrier change (keyed implicitly by xtc_self()).
203 *
204 * O(1), allocation-free, lock-free: a plain field access on the
205 * calling proc, which is running on this thread by definition. The
206 * runtime never dereferences or frees the pointer -- the consumer owns
207 * whatever it points to (the same lifetime contract as
208 * xtc_proc_at_exit's arg); it is simply dropped when the proc exits.
209 *
210 * xtc_proc_set_userdata returns XTC_OK, or XTC_E_INVAL when called off
211 * a proc (xtc_self() == XTC_PID_NONE). xtc_proc_userdata returns NULL
212 * off a proc.
213 */
214XTC_API int xtc_proc_set_userdata(void *ud);
215XTC_API void *xtc_proc_userdata(void);
216
217/*
218 * L1 proportional-share scheduler: place the CALLING proc's task in the
219 * scheduling class `cls` (created with xtc_exec_class_create on the
220 * proc's loop). The runtime-side equivalent of setting
221 * xtc_proc_opts_t.sched_class at spawn, but callable from inside a
222 * running proc so a proc can reclassify itself. Returns XTC_OK, or
223 * XTC_E_INVAL called off a proc or with a handle that is not on the
224 * calling proc's loop. INSPIRED BY Glommio's task-queue placement.
225 */
226XTC_API int xtc_proc_set_class(xtc_exec_class_t cls);
227
228/*
229 * Send a message. Copies `size` bytes from `data` into a mailbox
230 * envelope; the caller retains ownership of `data`. Returns:
231 * XTC_OK queued successfully
232 * XTC_E_INVAL NULL data with non-zero size, or stale/unknown pid
233 * XTC_E_AGAIN target mailbox at capacity
234 * XTC_E_RESOURCE global slot cap (XTC_RES_CHAN_SLOTS) hit
235 *
236 * BACKPRESSURE CONTRACT -- read this. Mailboxes are bounded (the
237 * cap is xtc_proc_opts_t.mailbox_cap, default 4096). This is
238 * deliberate: an unbounded mailbox is how an actor system OOMs when
239 * a fast sender outruns a slow receiver (the classic unbounded-mailbox failure).
240 * The price is that send can fail with XTC_E_AGAIN when the target
241 * is full, and a dropped XTC_E_AGAIN is a SILENT MESSAGE LOSS.
242 *
243 * Senders MUST check the return value and decide a policy:
244 * - retry later (re-arm on a timer, or yield and resend),
245 * - shed load (drop the message and account it),
246 * - apply end-to-end flow control (e.g. stop reading the upstream
247 * socket until the target drains -- see examples/07_kaka for a
248 * credit-based scheme), or
249 * - treat it as fatal for a must-deliver path.
250 * Ignoring the return is a bug, not a shortcut.
251 */
252XTC_API int xtc_send(xtc_pid_t to, const void *data, size_t size);
253
254/*
255 * Receive the next envelope from this process's mailbox. Allocates
256 * a new buffer for the caller (via the library allocator); the caller
257 * frees it with xtc_free. Blocks (yields the coroutine) up to
258 * timeout_ns; -1 is
259 * indefinite, 0 is non-blocking.
260 *
261 * Returns:
262 * XTC_OK *out / *size set
263 * XTC_E_AGAIN timeout fired with no message
264 * XTC_E_INVAL called outside a process
265 */
266XTC_API int xtc_recv(void **out, size_t *out_size, int64_t timeout_ns);
267
268/*
269 * Selective receive. match_fn is called for each envelope in
270 * arrival order; the first one for which match_fn returns 1 is
271 * delivered. Non-matching envelopes are kept in the save queue.
272 */
273XTC_API int xtc_recv_match(xtc_match_fn match_fn, void *user_data,
274 void **out, size_t *out_size,
275 int64_t timeout_ns);
276
277/*
278 * Receive `n_expected` messages whose leading `corr_size` bytes
279 * match `corr_value`. The first `n_expected` matching messages
280 * are delivered as an array via `out_msgs[]`; non-matching
281 * messages stay in the mailbox / save queue for subsequent
282 * receives. Returns XTC_OK on full collection; XTC_E_AGAIN if
283 * the timeout fires before n_expected matches arrive (in which
284 * case `*out_n` is the number actually collected; out_msgs[0..*out_n]
285 * are still owned by the caller and must be freed).
286 *
287 * This is the canonical helper for fork-join and request-reply
288 * patterns: pick a correlation id, send N children a request
289 * containing that id, wait for N replies whose first corr_size
290 * bytes equal the id. Avoids manual save-queue management.
291 *
292 * Each delivered message conforms to the same ownership contract
293 * as xtc_recv: the caller owns the buffer and must free() it.
294 */
295typedef struct xtc_msg {
296 void *data;
297 size_t size;
298} xtc_msg_t;
299
300XTC_API int xtc_recv_correlate(const void *corr_value, size_t corr_size,
301 int n_expected,
302 xtc_msg_t *out_msgs,
303 int *out_n,
304 int64_t timeout_ns);
305
306/*
307 * Wait until ANY of the following becomes true:
308 * - the given fd has any of `interest` bits set
309 * (XTC_IO_READABLE / WRITABLE / ERR / HUP),
310 * - a message arrives in the calling proc's mailbox,
311 * - the timeout elapses (only if `timeout_ns >= 0`),
312 * - the proc is killed (xtc_exit_pid raises the exit as usual).
313 *
314 * Returns:
315 * XTC_OK on a non-timeout wakeup. *out_revents has the
316 * XTC_IO_* bits that fired plus XTC_WAIT_MAILBOX if
317 * a message is queued. Multiple bits can be set if
318 * more than one source raced to wake.
319 * XTC_E_AGAIN timeout fired with nothing else. *out_revents has
320 * XTC_WAIT_TIMEOUT.
321 * XTC_E_INVAL bad args (NULL out_revents, fd<0, etc.) or called
322 * from outside a process.
323 *
324 * The fd is auto-unregistered before return; the mailbox is left
325 * untouched (caller still calls xtc_recv to actually drain).
326 */
327#define XTC_WAIT_MAILBOX 0x10000u /* in out_revents only */
328#define XTC_WAIT_TIMEOUT 0x20000u /* in out_revents only */
329
330XTC_API int xtc_proc_wait_fd(int fd, uint32_t interest, int64_t timeout_ns,
331 uint32_t *out_revents);
332
333/* Sleep the calling process for at least ns nanoseconds by parking it
334 * on a timer (the loop runs other work meanwhile -- it does not block
335 * the thread). Unlike a timed xtc_recv it does not touch the mailbox.
336 * Returns XTC_E_INVAL if not called from a process. */
337XTC_API int xtc_proc_sleep(int64_t ns);
338
339/* Explicit exit from inside a process; reason is delivered via
340 * EXIT/DOWN signals to linked / monitoring procs. */
341XTC_API int xtc_exit_self(int reason);
342
343/* Link / unlink: bidirectional fate. */
344XTC_API int xtc_link(xtc_pid_t other);
345XTC_API int xtc_unlink(xtc_pid_t other);
346
347/* ---- Monitor DOWN reasons ------------------------------------------
348 *
349 * A monitor's DOWN carries an int `reason` describing how the target
350 * ended. The reason space is:
351 *
352 * 0 -- clean exit (the target returned, or called
353 * xtc_exit_self(0)).
354 * > 0 -- an application exit code the target passed to
355 * xtc_exit_self(code) (including a contained fault
356 * that ran xtc_exit_self(sig), which passes the
357 * POSITIVE signal number, e.g. 11 for SIGSEGV).
358 * XTC_DOWN_NOPROC -- the monitor was registered on a target that had
359 * ALREADY exited (the monitor raced the target's
360 * exit), so its real exit reason was already reaped
361 * and is unknown. This is NOT a crash: a short-lived
362 * target that a supervisor monitors just after it
363 * finished delivers this, and it is expected. It is a
364 * distinct value (not XTC_E_NOTFOUND, and outside the
365 * 1..255 signal-number range) precisely so a
366 * supervisor can tell "already gone" apart from a real
367 * fault exit -- a DOWN reason of, say, 11 is a
368 * contained SIGSEGV, whereas XTC_DOWN_NOPROC is not.
369 *
370 * xtc_down_is_noproc(reason) is the readable test for the last case.
371 */
372#define XTC_DOWN_NOPROC (-100000)
373
374static inline int
375xtc_down_is_noproc(int reason)
376{
377 return reason == XTC_DOWN_NOPROC;
378}
379
380/* Monitor: unidirectional notification. out_ref is filled with the
381 * monitor reference; the watcher receives a DOWN message of shape
382 * { uint8_t kind = 'D'; uint64_t ref; xtc_pid_t pid; int reason; }
383 * when the monitored process exits (reason per the DOWN-reason space
384 * above). Registering a monitor on an ALREADY-dead target is not an
385 * error: it delivers an immediate DOWN with reason XTC_DOWN_NOPROC. */
386XTC_API int xtc_monitor(xtc_pid_t target, uint64_t *out_ref);
387
388/* Snapshot a process's mailbox statistics into *out. Returns XTC_OK,
389 * or XTC_E_INVAL if the pid is dead / unknown. Safe to call from any
390 * thread. */
391int xtc_proc_mailbox_stats(xtc_pid_t pid, xtc_mailbox_stats_t *out);
392
393/* ---- R1: per-fiber fault containment ----
394 *
395 * Turns a real synchronous fault (SIGSEGV / SIGBUS / SIGFPE / SIGILL
396 * on POSIX; the equivalent EXCEPTION_* on Windows) inside one
397 * coroutine into an unwind of only that process, leaving siblings on
398 * the same loop untouched -- the runtime support PG's "let it crash"
399 * session containment needs.
400 *
401 * POSIX: sigaltstack + sigaction + siglongjmp. Windows: a Vectored
402 * Exception Handler restores the CONTEXT captured at
403 * xtc_proc_recovery_arm() (no stack unwind). Both paths are
404 * runtime-verified on their hosts: a contained fault outside a
405 * critical section unwinds the one proc and delivers DOWN to its
406 * monitors; a fault inside a critical section escalates to process
407 * abort (POSIX re-raise; Windows EXCEPTION_CONTINUE_SEARCH ->
408 * 0xC0000005), preserving PG's critical-section PANIC semantics.
409 */
410#if (defined(__sun) || defined(__illumos__)) && !defined(__EXTENSIONS__)
411/* illumos/Solaris gate sigjmp_buf / sigsetjmp / siglongjmp behind a
412 * feature-test macro; under -std=c11 (strict ISO C) <setjmp.h> hides
413 * them. A consumer that includes this PUBLIC header need not know to
414 * set the macro itself, so expose the POSIX setjmp surface here before
415 * the include. No effect on other platforms. */
416#define __EXTENSIONS__ 1
417#endif
418#include <setjmp.h>
419
420#if defined(_WIN32)
421#include <windows.h>
422/*
423 * Windows recovery uses CONTEXT capture/restore rather than
424 * setjmp/longjmp. The fault is caught by a Vectored Exception Handler
425 * that restores this saved CONTEXT via EXCEPTION_CONTINUE_EXECUTION --
426 * the OS reloads the thread's registers and resumes at the capture
427 * point. longjmp out of (or via) a VEH is unsafe: on a fiber stack it
428 * walks unwind tables that no longer match and corrupts the CRT; a
429 * context restore does no unwinding at all.
430 */
431typedef struct xtc_recovery_buf { CONTEXT ctx; } xtc_recovery_buf_t;
432#else
433typedef sigjmp_buf xtc_recovery_buf_t;
434#endif
435
436/* Install the process-wide fault handler. On POSIX it registers a
437 * SIGSEGV/SIGBUS/SIGFPE/SIGILL handler on an alternate signal stack
438 * (call once per loop thread for the alt stack; the handler is
439 * installed once). On Windows it registers a Vectored Exception
440 * Handler. Returns XTC_OK on success. */
441int xtc_fault_guard_install(void);
442
443/* Internal arm-slot for the xtc_proc_recovery_arm() macro (POSIX). */
444xtc_recovery_buf_t *__xtc_proc_recovery_slot(void);
445
446#if defined(_WIN32)
447/* Windows recovery-arm helpers (used by the macro below).
448 * __xtc_recovery_prep arms the frame and clears the fired flag;
449 * __xtc_recovery_ctx returns the CONTEXT to capture into;
450 * __xtc_recovery_result returns 0 on the arming pass and the fault
451 * code when the VEH has restored the context. */
452void __xtc_recovery_prep(void);
453CONTEXT *__xtc_recovery_ctx(void);
454int __xtc_recovery_result(void);
455#endif
456
457/*
458 * Arm a recovery frame for the calling process, exactly like
459 * sigsetjmp: returns 0 on the normal path and the fault signal number
460 * (POSIX) or exception code (Windows) when control returns here via a
461 * contained fault. Use it as:
462 *
463 * int sig = xtc_proc_recovery_arm();
464 * if (sig != 0) { // recovered from a contained fault
465 * ... release locks, reset the memory context, close fds ...
466 * xtc_exit_self(reason); // delivers DOWN to the supervisor
467 * }
468 * ... session work ...
469 *
470 * The frame is disarmed automatically when a fault fires it (so a
471 * fault during recovery escalates to process abort); re-arm or
472 * xtc_proc_recovery_disarm() as needed.
473 *
474 * IMPORTANT: containment only unwinds the fiber's CALL STACK. Any
475 * resources the proc held at fault time -- locks, fds, allocations,
476 * buffer pins -- are the recovery block's responsibility to release:
477 * abort any in-progress transaction and release every held lock and
478 * resource before returning. Hold those under an xtc_mctx you
479 * can reset, and release lock-manager locks with the lock manager's
480 * release-all; otherwise a contained fault leaks or, worse, leaves a
481 * lock held and wedges peers.
482 */
483#if defined(_WIN32)
484/* Capture the proc fn's own frame inline (like setjmp), so the VEH can
485 * restore it; the comma expression returns 0 while arming and the
486 * fault code after a contained fault resumes execution here. */
487#define xtc_proc_recovery_arm() \
488 (__xtc_recovery_prep(), \
489 RtlCaptureContext(__xtc_recovery_ctx()), \
490 __xtc_recovery_result())
491#else
492#define xtc_proc_recovery_arm() (sigsetjmp(*__xtc_proc_recovery_slot(), 1))
493#endif
494
495/* Disarm the calling process's recovery frame. */
496void xtc_proc_recovery_disarm(void);
497
498/* Critical section: while crit_depth > 0, a fault is NOT contained --
499 * it escalates to process abort, because shared state may be torn.
500 * Nestable. Mirrors PG's START_CRIT_SECTION / END_CRIT_SECTION. */
501void xtc_proc_critical_enter(void);
502void xtc_proc_critical_leave(void);
503
504/* Register a callback to run when the calling process exits -- on a
505 * normal return OR a contained-fault recovery (after
506 * xtc_proc_recovery_arm -> xtc_exit_self). Callbacks run LIFO,
507 * outside signal context, with the proc still current, BEFORE its
508 * monitors observe DOWN. This is where an embedder guarantees a
509 * faulted session releases what it held -- e.g. register
510 * xtc_lock_release_all so no lock-manager lock outlives the proc, or
511 * a memory-context reset. Up to a small fixed number per proc;
512 * returns XTC_E_RESOURCE past the limit, XTC_E_INVAL off a proc. */
513int xtc_proc_at_exit(void (*fn)(void *), void *arg);
514
515/* A memory context scoped to the calling process: created lazily on
516 * first call, destroyed automatically on proc exit (a backstop so a
517 * faulted session's allocations are reclaimed even if its recovery
518 * block itself faults). Returns NULL off a proc. See xtc_mctx.h. */
519struct xtc_mctx *xtc_proc_mctx(void);
520
521/*
522 * Recovery resource registry.
523 *
524 * A contained fault unwinds the faulting fiber's call stack but frees
525 * NONE of the resources the proc held -- locks stay locked, fds stay
526 * open, memory arenas stay live -- and a leaked lock can wedge every
527 * peer. Register the resources a proc acquires so the runtime can
528 * release them automatically:
529 *
530 * xtc_proc_recovery_track_fd(fd); // close(fd) on cleanup
531 * xtc_proc_recovery_track_mctx(mctx); // xtc_mctx_reset(mctx)
532 * xtc_proc_recovery_track_locks(mgr, locker, release_all);
533 * xtc_proc_recovery_track(fn, arg); // generic fn(arg)
534 *
535 * xtc_proc_recovery_cleanup() releases everything registered (LIFO).
536 * It is the DEFAULT recovery action and is ALSO callable from a custom
537 * recovery block to finish the standard bits after the block's own
538 * application-specific unwinding:
539 *
540 * int sig = xtc_proc_recovery_arm();
541 * if (sig != 0) { // recovered from a contained fault
542 * my_app_abort_txn(); // custom unwinding first
543 * xtc_proc_recovery_cleanup(); // then the registered standard bits
544 * xtc_exit_self(sig);
545 * }
546 *
547 * Or use xtc_proc_recovery_arm_clean(), which performs the cleanup and
548 * xtc_exit_self automatically on the recovered branch (no custom block).
549 *
550 * Registered resources are ALSO released automatically on a NORMAL
551 * proc exit (before the at-exit hooks), so a proc that simply returns
552 * without explicitly releasing still cleans up. Use
553 * xtc_proc_recovery_untrack_fd() when the proc releases an fd itself,
554 * to avoid a double close on a later recovery.
555 *
556 * Each registration returns XTC_OK, XTC_E_RESOURCE past the per-proc
557 * limit, or XTC_E_INVAL off a proc / on a bad argument.
558 */
559int xtc_proc_recovery_track_fd(int fd);
560int xtc_proc_recovery_track_mctx(struct xtc_mctx *mctx);
561int xtc_proc_recovery_track_locks(void *mgr, uint64_t locker,
562 void (*release_all)(void *, uint64_t));
563int xtc_proc_recovery_track(void (*fn)(void *), void *arg);
564int xtc_proc_recovery_untrack_fd(int fd);
565void xtc_proc_recovery_cleanup(void);
566
567/*
568 * Arm a recovery frame whose default recovered action is to release
569 * all tracked resources and exit the proc with the fault code. On the
570 * normal (arming) pass it returns 0 and execution continues into the
571 * session work; on a contained fault it does NOT return -- it runs
572 * xtc_proc_recovery_cleanup() then xtc_exit_self(sig). Use this when
573 * the registered resources are the whole cleanup story; use the bare
574 * xtc_proc_recovery_arm() + a custom block when they are not.
575 */
576#define xtc_proc_recovery_arm_clean() \
577 do { \
578 int __xtc_rsig = xtc_proc_recovery_arm(); \
579 if (__xtc_rsig != 0) { \
580 xtc_proc_recovery_cleanup(); \
581 (void)xtc_exit_self(__xtc_rsig); \
582 } \
583 } while (0)
584
585/* ---- A2: cancellation masking (uncancelable / poll) ----
586 *
587 * MonadCancel's masking discipline, for C. Structured cancellation via
588 * xtc_abort / xtc_exit_pid is cooperative -- a running fiber observes it
589 * only at a yield/recv point. Masking makes it COMPOSABLE: inside an
590 * xtc_uncancelable() region an asynchronous kill delivered at a park
591 * point is DEFERRED, not acted on, and is observed only when the region
592 * returns (mask depth back to 0). This is what lets a resource acquired
593 * in the region always register its release before cancellation unwinds
594 * the fiber -- the guarantee xtc_scope / xtc_bracket rely on.
595 *
596 * Run `body(ud)` with cancellation masked; returns body's return value
597 * (or XTC_E_INVAL if body is NULL). Nests: each call bumps a per-proc
598 * mask counter. Off a proc it simply runs body.
599 *
600 * PUBLIC: int xtc_uncancelable __P((int (*)(void *), void *));
601 */
602XTC_API int xtc_uncancelable(int (*body)(void *), void *ud);
603
604/*
605 * Cats Effect's poll: inside an xtc_uncancelable() body, run `body(ud)`
606 * with the mask temporarily lifted so cancellation IS observed for that
607 * sub-region. A kill already deferred by the enclosing mask fires at
608 * the poll site (before body runs); the caller's mask depth is restored
609 * on return. Outside a masked region it just runs body. Returns body's
610 * value (or XTC_E_INVAL if body is NULL).
611 *
612 * PUBLIC: int xtc_cancel_poll __P((int (*)(void *), void *));
613 */
614XTC_API int xtc_cancel_poll(int (*body)(void *), void *ud);
615
616/*
617 * True iff cancellation is pending for the calling proc -- either an
618 * async kill has been requested (xtc_exit_pid) or one was deferred by a
619 * mask. A cheap, allocation-free probe for a masked region that wants
620 * to unwind early and cleanly rather than wait for the park point.
621 * Returns 0 off a proc.
622 *
623 * PUBLIC: int xtc_cancel_requested __P((void));
624 */
625XTC_API int xtc_cancel_requested(void);
626
627/* ---- A1: resource scope / bracket ----
628 *
629 * A blessed, runtime-ENFORCED resource scope. Cats Effect's
630 * Resource/bracket was, for years, a "paper door": a convention the API
631 * carroted you toward but nothing stopped you walking past, leaking a
632 * socket on the cancellation path. xtc_scope makes
633 * "this WILL be released on EVERY exit path" a MECHANISM, not a manner.
634 *
635 * Open a scope on the calling proc, defer finalizers into it, and close
636 * it. Finalizers run LIFO -- on the normal xtc_scope_close, AND on an
637 * error return, xtc_exit_self, an async kill (xtc_exit_pid), or a
638 * fault-guard-contained crash, because the scope is pushed as a marker
639 * on the same recovery registry that releases fds and locks on unwind.
640 * Scopes nest; an outer unwind closes inner-then-outer LIFO.
641 *
642 * xtc_scope_open returns NULL off a proc or on resource exhaustion.
643 */
644typedef struct xtc_scope xtc_scope_t;
645typedef void (*xtc_finalizer_fn)(void *arg);
646
647/*
648 * PUBLIC: xtc_scope_t *xtc_scope_open __P((void));
649 * PUBLIC: int xtc_scope_defer __P((xtc_scope_t *, xtc_finalizer_fn, void *));
650 * PUBLIC: void xtc_scope_close __P((xtc_scope_t *));
651 */
652XTC_API xtc_scope_t *xtc_scope_open(void);
653XTC_API int xtc_scope_defer(xtc_scope_t *s, xtc_finalizer_fn fn, void *arg);
654XTC_API void xtc_scope_close(xtc_scope_t *s);
655
656/*
657 * bracket sugar (acquire -> use -> guaranteed release), the load-bearing
658 * correctness core. `acquire` runs abort-MASKED so `release` is always
659 * registered before cancellation can be observed; `release` then runs on
660 * EVERY exit path of `use` (normal, error, kill, crash). acquire writes
661 * the resource through *res and returns XTC_OK (else bracket returns its
662 * code and nothing else runs); use returns a code that bracket returns;
663 * release is void. `ud` threads a caller context through all three.
664 *
665 * PUBLIC: int xtc_bracket __P((int (*)(void **, void *), int (*)(void *, void *), void (*)(void *, void *), void *));
666 */
667XTC_API int xtc_bracket(int (*acquire)(void **res, void *ud),
668 int (*use)(void *res, void *ud),
669 void (*release)(void *res, void *ud),
670 void *ud);
671
672/* Decode a DOWN signal (delivered to a monitor when its target exits)
673 * into the target pid and exit reason, without hand-rolling the
674 * on-wire layout. The DOWN/EXIT signals are sent packed; a mismatched
675 * (unpacked) mirror struct misreads `reason`. Returns XTC_OK if msg
676 * is a DOWN, XTC_E_INVAL otherwise. out_pid / out_reason may be NULL. */
677int xtc_down_decode(const void *msg, size_t len,
678 xtc_pid_t *out_pid, int *out_reason);
679
680/*
681 * Self-describing DOWN classification (requested by embedders whose
682 * app exit codes and signal numbers would otherwise share the single
683 * `reason` integer: a bare xtc_exit_self(1) was indistinguishable from
684 * a signal-1 (SIGHUP) contained fault). xtc_down_decode_ex fills an
685 * xtc_down_info_t whose `kind` says HOW the target ended, with the
686 * signal number and the app exit code in SEPARATE fields that never
687 * collide. The legacy single-integer xtc_down_decode still works and
688 * returns the same `reason` it always did.
689 */
690typedef enum {
691 XTC_DOWN_KIND_CLEAN = 0, /* target returned or xtc_exit_self(0) */
692 XTC_DOWN_KIND_EXIT = 1, /* xtc_exit_self(code), code in .exit_code */
693 XTC_DOWN_KIND_SIGNAL = 2, /* R1 contained fault, signal in .signal */
694 XTC_DOWN_KIND_NOPROC = 3, /* monitor raced a dead target (benign) */
695 XTC_DOWN_KIND_NOCONNECTION = 4 /* the cross-process channel died
696 * before a clean exit was seen
697 * (xtc_xproc: the fork'd child's
698 * control socket closed) */
699} xtc_down_kind_t;
700
701typedef struct {
702 xtc_pid_t pid; /* the target that went DOWN */
703 xtc_down_kind_t kind; /* how it ended (never ambiguous) */
704 int signal; /* signal number iff kind == SIGNAL, else 0 */
705 int exit_code; /* app code iff kind == EXIT, else 0 */
706 int reason; /* the legacy xtc_down_decode reason value */
707 uint64_t ref; /* the monitor reference (0 for a link EXIT) */
709
710/*
711 * Decode a DOWN or EXIT signal into a fully-classified xtc_down_info_t.
712 * Accepts both the monitor DOWN ('D') and the link EXIT ('E') signal
713 * shapes. Returns XTC_OK on either, XTC_E_INVAL for any other message.
714 * A monitor need no longer know the producer's encoding convention:
715 * kind + signal + exit_code are unambiguous by construction.
716 *
717 * PUBLIC: int xtc_down_decode_ex __P((const void *, size_t, xtc_down_info_t *));
718 */
719XTC_API int xtc_down_decode_ex(const void *msg, size_t len,
720 xtc_down_info_t *out);
721
722/* Predicate + accessor helpers over a legacy `reason` integer, for
723 * callers that keep using xtc_down_decode. A signal-N fault and a
724 * bare xtc_exit_self(N) are STILL not distinguishable from `reason`
725 * alone (that is the whole reason to prefer xtc_down_decode_ex); these
726 * helpers only classify the two unambiguous sentinels. */
727static inline int xtc_down_is_signal_reason(int reason)
728{
729 return reason >= 1 && reason <= 255;
730}
731static inline int xtc_down_is_exit_reason(int reason)
732{
733 return reason == 0 || reason >= 256;
734}
735
736/* The internal current-proc context save/restore across a yield
737 * (__xtc_proc_ctx_save / __xtc_proc_ctx_restore) is library-internal
738 * (the __ prefix) and lives in "proc_int.h", not in this installed
739 * public header. */
740
741#endif /* XTC_PROC_H */