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 <stddef.h>
22#include <stdint.h>
23
24#include "xtc.h"
25#include "xtc_loop.h"
26#include "xtc_async.h"
27
28/*
29 * Process identifier. Encodes the loop ID, a per-loop slot index,
30 * and a generation counter so a stale pid (after a process exits and
31 * its slot is reused) is recognisably stale on lookup.
32 */
33typedef struct xtc_pid {
34 uint16_t loop_id;
35 uint16_t local_id;
36 uint32_t gen;
37} xtc_pid_t;
38
39#define XTC_PID_NONE ((xtc_pid_t){0, 0, 0})
40
41static inline int
42xtc_pid_eq(xtc_pid_t a, xtc_pid_t b)
43{
44 return a.loop_id == b.loop_id &&
45 a.local_id == b.local_id &&
46 a.gen == b.gen;
47}
48
49static inline int
50xtc_pid_is_none(xtc_pid_t p)
51{
52 return p.loop_id == 0 && p.local_id == 0 && p.gen == 0;
53}
54
55/*
56 * Match callback for selective receive. Inspects an envelope's
57 * data + size and returns:
58 * 1 -> consume this envelope (the receive call returns it)
59 * 0 -> skip; envelope stays in the save queue for the next receive
60 */
61typedef int (*xtc_match_fn)(const void *data, size_t size, void *user_data);
62
63/*
64 * Process entry function. The proc runs as a coroutine and the
65 * function returns nothing; exit happens by returning from the entry,
66 * by calling xtc_exit, or by being killed via a link.
67 */
68typedef void (*xtc_proc_fn)(void *arg);
69
70typedef struct xtc_proc_opts {
71 const char *name; /* optional, for debug */
72 size_t mailbox_cap; /* 0 = default */
73 int link_to; /* if != 0, this is a pid index to link to */
74 /* Mailbox watermark: when an accepted message brings the depth to
75 * this percent of mailbox_cap (1..100; 0 = disabled), the callback
76 * fires once on the rising edge, so the app can shed load before
77 * the hard cap rejects with XTC_E_AGAIN. The callback runs on the
78 * sender's thread, outside the mailbox lock; keep it cheap and do
79 * not block. */
80 int mailbox_watermark_pct;
81 void (*mailbox_watermark_fn)(xtc_pid_t self, size_t depth,
82 size_t cap, void *user);
83 void *mailbox_watermark_user;
85
86/* Mailbox statistics snapshot (see xtc_proc_mailbox_stats). */
87typedef struct xtc_mailbox_stats {
88 size_t depth; /* messages currently in the mailbox */
89 size_t saved; /* messages held in the selective-receive
90 * save queue (inspected, not yet matched) */
91 size_t peak; /* high-water mailbox depth ever reached */
92 size_t cap; /* capacity bound on depth + saved (0 = none) */
93 uint64_t recv_total; /* messages accepted over the proc's life */
94 uint64_t drop_total; /* messages rejected (full / dead) */
96
97/*
98 * PUBLIC: int xtc_proc_spawn __P((xtc_loop_t *, xtc_proc_fn, void *, const xtc_proc_opts_t *, xtc_pid_t *));
99 * PUBLIC: int xtc_proc_spawn_link __P((xtc_loop_t *, xtc_proc_fn, void *, const xtc_proc_opts_t *, xtc_pid_t *));
100 * PUBLIC: int xtc_proc_spawn_monitor __P((xtc_loop_t *, xtc_proc_fn, void *, const xtc_proc_opts_t *, xtc_pid_t *, uint64_t *));
101 * PUBLIC: xtc_pid_t xtc_self __P((void));
102 * PUBLIC: int xtc_send __P((xtc_pid_t, const void *, size_t));
103 * PUBLIC: int xtc_recv __P((void **, size_t *, int64_t));
104 * PUBLIC: int xtc_recv_match __P((xtc_match_fn, void *, void **, size_t *, int64_t));
105 * PUBLIC: int xtc_recv_correlate __P((const void *, size_t, int, xtc_msg_t *, int *, int64_t));
106 * PUBLIC: int xtc_proc_wait_fd __P((int, uint32_t, int64_t, uint32_t *));
107 * PUBLIC: int xtc_proc_sleep __P((int64_t));
108 * PUBLIC: int xtc_exit_self __P((int));
109 * PUBLIC: int xtc_exit_pid __P((xtc_pid_t, int));
110 * PUBLIC: int xtc_proc_wake __P((xtc_pid_t));
111 * PUBLIC: int xtc_link __P((xtc_pid_t));
112 * PUBLIC: int xtc_unlink __P((xtc_pid_t));
113 * PUBLIC: int xtc_monitor __P((xtc_pid_t, uint64_t *));
114 */
115
116int xtc_proc_spawn(xtc_loop_t *loop, xtc_proc_fn fn, void *arg,
117 const xtc_proc_opts_t *opts, xtc_pid_t *out_pid);
118
119/*
120 * Atomic spawn + link / spawn + monitor (Erlang spawn_link /
121 * spawn_monitor). Identical to xtc_proc_spawn, but the parent<->child
122 * relationship is established BEFORE the child is made runnable, so
123 * there is no window in which the child exists but is not yet
124 * linked/monitored -- even if the child runs and exits immediately,
125 * its EXIT/DOWN is delivered (no XTC_DOWN_NOPROC race that a
126 * spawn-then-link/monitor idiom can hit). The CALLER MUST be a
127 * process (xtc_self() != NONE); returns XTC_E_INVAL otherwise.
128 *
129 * _link: bidirectional fate, exactly like calling xtc_link(child)
130 * the instant the child is born -- an abnormal exit on either
131 * side raises an EXIT on the other.
132 * _monitor: unidirectional; the caller receives a DOWN (with *out_ref
133 * as the monitor reference) when the child exits, exactly
134 * like xtc_monitor(child).
135 */
136int xtc_proc_spawn_link(xtc_loop_t *loop, xtc_proc_fn fn, void *arg,
137 const xtc_proc_opts_t *opts, xtc_pid_t *out_pid);
138int xtc_proc_spawn_monitor(xtc_loop_t *loop, xtc_proc_fn fn,
139 void *arg, const xtc_proc_opts_t *opts,
140 xtc_pid_t *out_pid, uint64_t *out_ref);
141
142/* From inside a process, return its pid; from outside, returns NONE. */
143/*
144 * Asynchronous cross-process exit signal. Sets a kill flag on the
145 * target proc; the target raises the exit at its next yield/recv
146 * point with the supplied reason. Idempotent (first call wins).
147 * Returns XTC_E_INVAL if the target is unknown or already dead.
148 */
149int xtc_exit_pid(xtc_pid_t target, int reason);
150
151/*
152 * Resume a process parked in xtc_proc_wait_fd / xtc_recv, from ANY OS
153 * thread (including a thread libxtc does not manage). The explicit
154 * "poke the target loop" primitive: after a foreign thread makes a
155 * watched condition true (writes a self-pipe an fd-park watches, sets
156 * an embedder latch, completes an async read), xtc_proc_wake(pid)
157 * nudges the target loop out of its I/O wait so the parked proc
158 * re-checks its condition. It delivers no message; the woken proc just
159 * re-evaluates, so a spurious wake is always safe. Returns XTC_OK
160 * (including when the target is not parked, already runnable, or gone),
161 * XTC_E_INVAL for XTC_PID_NONE.
162 */
163int xtc_proc_wake(xtc_pid_t target);
164
165xtc_pid_t xtc_self(void);
166
167/*
168 * Send a message. Copies `size` bytes from `data` into a mailbox
169 * envelope; the caller retains ownership of `data`. Returns:
170 * XTC_OK queued successfully
171 * XTC_E_INVAL NULL data with non-zero size, or stale/unknown pid
172 * XTC_E_AGAIN target mailbox at capacity
173 * XTC_E_RESOURCE global slot cap (XTC_RES_CHAN_SLOTS) hit
174 *
175 * BACKPRESSURE CONTRACT -- read this. Mailboxes are bounded (the
176 * cap is xtc_proc_opts_t.mailbox_cap, default 4096). This is
177 * deliberate: an unbounded mailbox is how an actor system OOMs when
178 * a fast sender outruns a slow receiver (the classic unbounded-mailbox failure).
179 * The price is that send can fail with XTC_E_AGAIN when the target
180 * is full, and a dropped XTC_E_AGAIN is a SILENT MESSAGE LOSS.
181 *
182 * Senders MUST check the return value and decide a policy:
183 * - retry later (re-arm on a timer, or yield and resend),
184 * - shed load (drop the message and account it),
185 * - apply end-to-end flow control (e.g. stop reading the upstream
186 * socket until the target drains -- see examples/07_kaka for a
187 * credit-based scheme), or
188 * - treat it as fatal for a must-deliver path.
189 * Ignoring the return is a bug, not a shortcut.
190 */
191int xtc_send(xtc_pid_t to, const void *data, size_t size);
192
193/*
194 * Receive the next envelope from this process's mailbox. Allocates
195 * a new buffer for the caller (via the library allocator); the caller
196 * frees it with xtc_free. Blocks (yields the coroutine) up to
197 * timeout_ns; -1 is
198 * indefinite, 0 is non-blocking.
199 *
200 * Returns:
201 * XTC_OK *out / *size set
202 * XTC_E_AGAIN timeout fired with no message
203 * XTC_E_INVAL called outside a process
204 */
205int xtc_recv(void **out, size_t *out_size, int64_t timeout_ns);
206
207/*
208 * Selective receive. match_fn is called for each envelope in
209 * arrival order; the first one for which match_fn returns 1 is
210 * delivered. Non-matching envelopes are kept in the save queue.
211 */
212int xtc_recv_match(xtc_match_fn match_fn, void *user_data,
213 void **out, size_t *out_size,
214 int64_t timeout_ns);
215
216/*
217 * Receive `n_expected` messages whose leading `corr_size` bytes
218 * match `corr_value`. The first `n_expected` matching messages
219 * are delivered as an array via `out_msgs[]`; non-matching
220 * messages stay in the mailbox / save queue for subsequent
221 * receives. Returns XTC_OK on full collection; XTC_E_AGAIN if
222 * the timeout fires before n_expected matches arrive (in which
223 * case `*out_n` is the number actually collected; out_msgs[0..*out_n]
224 * are still owned by the caller and must be freed).
225 *
226 * This is the canonical helper for fork-join and request-reply
227 * patterns: pick a correlation id, send N children a request
228 * containing that id, wait for N replies whose first corr_size
229 * bytes equal the id. Avoids manual save-queue management.
230 *
231 * Each delivered message conforms to the same ownership contract
232 * as xtc_recv: the caller owns the buffer and must free() it.
233 */
234typedef struct xtc_msg {
235 void *data;
236 size_t size;
237} xtc_msg_t;
238
239int xtc_recv_correlate(const void *corr_value, size_t corr_size,
240 int n_expected,
241 xtc_msg_t *out_msgs,
242 int *out_n,
243 int64_t timeout_ns);
244
245/*
246 * Wait until ANY of the following becomes true:
247 * - the given fd has any of `interest` bits set
248 * (XTC_IO_READABLE / WRITABLE / ERR / HUP),
249 * - a message arrives in the calling proc's mailbox,
250 * - the timeout elapses (only if `timeout_ns >= 0`),
251 * - the proc is killed (xtc_exit_pid raises the exit as usual).
252 *
253 * Returns:
254 * XTC_OK on a non-timeout wakeup. *out_revents has the
255 * XTC_IO_* bits that fired plus XTC_WAIT_MAILBOX if
256 * a message is queued. Multiple bits can be set if
257 * more than one source raced to wake.
258 * XTC_E_AGAIN timeout fired with nothing else. *out_revents has
259 * XTC_WAIT_TIMEOUT.
260 * XTC_E_INVAL bad args (NULL out_revents, fd<0, etc.) or called
261 * from outside a process.
262 *
263 * The fd is auto-unregistered before return; the mailbox is left
264 * untouched (caller still calls xtc_recv to actually drain).
265 */
266#define XTC_WAIT_MAILBOX 0x10000u /* in out_revents only */
267#define XTC_WAIT_TIMEOUT 0x20000u /* in out_revents only */
268
269int xtc_proc_wait_fd(int fd, uint32_t interest, int64_t timeout_ns,
270 uint32_t *out_revents);
271
272/* Sleep the calling process for at least ns nanoseconds by parking it
273 * on a timer (the loop runs other work meanwhile -- it does not block
274 * the thread). Unlike a timed xtc_recv it does not touch the mailbox.
275 * Returns XTC_E_INVAL if not called from a process. */
276int xtc_proc_sleep(int64_t ns);
277
278/* Explicit exit from inside a process; reason is delivered via
279 * EXIT/DOWN signals to linked / monitoring procs. */
280int xtc_exit_self(int reason);
281
282/* Link / unlink: bidirectional fate. */
283int xtc_link(xtc_pid_t other);
284int xtc_unlink(xtc_pid_t other);
285
286/* ---- Monitor DOWN reasons ------------------------------------------
287 *
288 * A monitor's DOWN carries an int `reason` describing how the target
289 * ended. The reason space is:
290 *
291 * 0 -- clean exit (the target returned, or called
292 * xtc_exit_self(0)).
293 * > 0 -- an application exit code the target passed to
294 * xtc_exit_self(code) (including a contained fault
295 * that ran xtc_exit_self(sig), which passes the
296 * POSITIVE signal number, e.g. 11 for SIGSEGV).
297 * XTC_DOWN_NOPROC -- the monitor was registered on a target that had
298 * ALREADY exited (the monitor raced the target's
299 * exit), so its real exit reason was already reaped
300 * and is unknown. This is NOT a crash: a short-lived
301 * target that a supervisor monitors just after it
302 * finished delivers this, and it is expected. It is a
303 * distinct value (not XTC_E_NOTFOUND, and outside the
304 * 1..255 signal-number range) precisely so a
305 * supervisor can tell "already gone" apart from a real
306 * fault exit -- a DOWN reason of, say, 11 is a
307 * contained SIGSEGV, whereas XTC_DOWN_NOPROC is not.
308 *
309 * xtc_down_is_noproc(reason) is the readable test for the last case.
310 */
311#define XTC_DOWN_NOPROC (-100000)
312
313static inline int
314xtc_down_is_noproc(int reason)
315{
316 return reason == XTC_DOWN_NOPROC;
317}
318
319/* Monitor: unidirectional notification. out_ref is filled with the
320 * monitor reference; the watcher receives a DOWN message of shape
321 * { uint8_t kind = 'D'; uint64_t ref; xtc_pid_t pid; int reason; }
322 * when the monitored process exits (reason per the DOWN-reason space
323 * above). Registering a monitor on an ALREADY-dead target is not an
324 * error: it delivers an immediate DOWN with reason XTC_DOWN_NOPROC. */
325int xtc_monitor(xtc_pid_t target, uint64_t *out_ref);
326
327/* Snapshot a process's mailbox statistics into *out. Returns XTC_OK,
328 * or XTC_E_INVAL if the pid is dead / unknown. Safe to call from any
329 * thread. */
330int xtc_proc_mailbox_stats(xtc_pid_t pid, xtc_mailbox_stats_t *out);
331
332/* ---- R1: per-fiber fault containment ----
333 *
334 * Turns a real synchronous fault (SIGSEGV / SIGBUS / SIGFPE / SIGILL
335 * on POSIX; the equivalent EXCEPTION_* on Windows) inside one
336 * coroutine into an unwind of only that process, leaving siblings on
337 * the same loop untouched -- the runtime support PG's "let it crash"
338 * session containment needs.
339 *
340 * POSIX: sigaltstack + sigaction + siglongjmp. Windows: a Vectored
341 * Exception Handler restores the CONTEXT captured at
342 * xtc_proc_recovery_arm() (no stack unwind). Both paths are
343 * runtime-verified on their hosts: a contained fault outside a
344 * critical section unwinds the one proc and delivers DOWN to its
345 * monitors; a fault inside a critical section escalates to process
346 * abort (POSIX re-raise; Windows EXCEPTION_CONTINUE_SEARCH ->
347 * 0xC0000005), preserving PG's critical-section PANIC semantics.
348 */
349#if (defined(__sun) || defined(__illumos__)) && !defined(__EXTENSIONS__)
350/* illumos/Solaris gate sigjmp_buf / sigsetjmp / siglongjmp behind a
351 * feature-test macro; under -std=c11 (strict ISO C) <setjmp.h> hides
352 * them. A consumer that includes this PUBLIC header need not know to
353 * set the macro itself, so expose the POSIX setjmp surface here before
354 * the include. No effect on other platforms. */
355#define __EXTENSIONS__ 1
356#endif
357#include <setjmp.h>
358
359#if defined(_WIN32)
360#include <windows.h>
361/*
362 * Windows recovery uses CONTEXT capture/restore rather than
363 * setjmp/longjmp. The fault is caught by a Vectored Exception Handler
364 * that restores this saved CONTEXT via EXCEPTION_CONTINUE_EXECUTION --
365 * the OS reloads the thread's registers and resumes at the capture
366 * point. longjmp out of (or via) a VEH is unsafe: on a fiber stack it
367 * walks unwind tables that no longer match and corrupts the CRT; a
368 * context restore does no unwinding at all.
369 */
370typedef struct xtc_recovery_buf { CONTEXT ctx; } xtc_recovery_buf_t;
371#else
372typedef sigjmp_buf xtc_recovery_buf_t;
373#endif
374
375/* Install the process-wide fault handler. On POSIX it registers a
376 * SIGSEGV/SIGBUS/SIGFPE/SIGILL handler on an alternate signal stack
377 * (call once per loop thread for the alt stack; the handler is
378 * installed once). On Windows it registers a Vectored Exception
379 * Handler. Returns XTC_OK on success. */
380int xtc_fault_guard_install(void);
381
382/* Internal arm-slot for the xtc_proc_recovery_arm() macro (POSIX). */
383xtc_recovery_buf_t *__xtc_proc_recovery_slot(void);
384
385#if defined(_WIN32)
386/* Windows recovery-arm helpers (used by the macro below).
387 * __xtc_recovery_prep arms the frame and clears the fired flag;
388 * __xtc_recovery_ctx returns the CONTEXT to capture into;
389 * __xtc_recovery_result returns 0 on the arming pass and the fault
390 * code when the VEH has restored the context. */
391void __xtc_recovery_prep(void);
392CONTEXT *__xtc_recovery_ctx(void);
393int __xtc_recovery_result(void);
394#endif
395
396/*
397 * Arm a recovery frame for the calling process, exactly like
398 * sigsetjmp: returns 0 on the normal path and the fault signal number
399 * (POSIX) or exception code (Windows) when control returns here via a
400 * contained fault. Use it as:
401 *
402 * int sig = xtc_proc_recovery_arm();
403 * if (sig != 0) { // recovered from a contained fault
404 * ... release locks, reset the memory context, close fds ...
405 * xtc_exit_self(reason); // delivers DOWN to the supervisor
406 * }
407 * ... session work ...
408 *
409 * The frame is disarmed automatically when a fault fires it (so a
410 * fault during recovery escalates to process abort); re-arm or
411 * xtc_proc_recovery_disarm() as needed.
412 *
413 * IMPORTANT: containment only unwinds the fiber's CALL STACK. Any
414 * resources the proc held at fault time -- locks, fds, allocations,
415 * buffer pins -- are the recovery block's responsibility to release:
416 * abort any in-progress transaction and release every held lock and
417 * resource before returning. Hold those under an xtc_mctx you
418 * can reset, and release lock-manager locks with the lock manager's
419 * release-all; otherwise a contained fault leaks or, worse, leaves a
420 * lock held and wedges peers.
421 */
422#if defined(_WIN32)
423/* Capture the proc fn's own frame inline (like setjmp), so the VEH can
424 * restore it; the comma expression returns 0 while arming and the
425 * fault code after a contained fault resumes execution here. */
426#define xtc_proc_recovery_arm() \
427 (__xtc_recovery_prep(), \
428 RtlCaptureContext(__xtc_recovery_ctx()), \
429 __xtc_recovery_result())
430#else
431#define xtc_proc_recovery_arm() (sigsetjmp(*__xtc_proc_recovery_slot(), 1))
432#endif
433
434/* Disarm the calling process's recovery frame. */
435void xtc_proc_recovery_disarm(void);
436
437/* Critical section: while crit_depth > 0, a fault is NOT contained --
438 * it escalates to process abort, because shared state may be torn.
439 * Nestable. Mirrors PG's START_CRIT_SECTION / END_CRIT_SECTION. */
440void xtc_proc_critical_enter(void);
441void xtc_proc_critical_leave(void);
442
443/* Register a callback to run when the calling process exits -- on a
444 * normal return OR a contained-fault recovery (after
445 * xtc_proc_recovery_arm -> xtc_exit_self). Callbacks run LIFO,
446 * outside signal context, with the proc still current, BEFORE its
447 * monitors observe DOWN. This is where an embedder guarantees a
448 * faulted session releases what it held -- e.g. register
449 * xtc_lock_release_all so no lock-manager lock outlives the proc, or
450 * a memory-context reset. Up to a small fixed number per proc;
451 * returns XTC_E_RESOURCE past the limit, XTC_E_INVAL off a proc. */
452int xtc_proc_at_exit(void (*fn)(void *), void *arg);
453
454/* A memory context scoped to the calling process: created lazily on
455 * first call, destroyed automatically on proc exit (a backstop so a
456 * faulted session's allocations are reclaimed even if its recovery
457 * block itself faults). Returns NULL off a proc. See xtc_mctx.h. */
458struct xtc_mctx *xtc_proc_mctx(void);
459
460/*
461 * Recovery resource registry.
462 *
463 * A contained fault unwinds the faulting fiber's call stack but frees
464 * NONE of the resources the proc held -- locks stay locked, fds stay
465 * open, memory arenas stay live -- and a leaked lock can wedge every
466 * peer. Register the resources a proc acquires so the runtime can
467 * release them automatically:
468 *
469 * xtc_proc_recovery_track_fd(fd); // close(fd) on cleanup
470 * xtc_proc_recovery_track_mctx(mctx); // xtc_mctx_reset(mctx)
471 * xtc_proc_recovery_track_locks(mgr, locker, release_all);
472 * xtc_proc_recovery_track(fn, arg); // generic fn(arg)
473 *
474 * xtc_proc_recovery_cleanup() releases everything registered (LIFO).
475 * It is the DEFAULT recovery action and is ALSO callable from a custom
476 * recovery block to finish the standard bits after the block's own
477 * application-specific unwinding:
478 *
479 * int sig = xtc_proc_recovery_arm();
480 * if (sig != 0) { // recovered from a contained fault
481 * my_app_abort_txn(); // custom unwinding first
482 * xtc_proc_recovery_cleanup(); // then the registered standard bits
483 * xtc_exit_self(sig);
484 * }
485 *
486 * Or use xtc_proc_recovery_arm_clean(), which performs the cleanup and
487 * xtc_exit_self automatically on the recovered branch (no custom block).
488 *
489 * Registered resources are ALSO released automatically on a NORMAL
490 * proc exit (before the at-exit hooks), so a proc that simply returns
491 * without explicitly releasing still cleans up. Use
492 * xtc_proc_recovery_untrack_fd() when the proc releases an fd itself,
493 * to avoid a double close on a later recovery.
494 *
495 * Each registration returns XTC_OK, XTC_E_RESOURCE past the per-proc
496 * limit, or XTC_E_INVAL off a proc / on a bad argument.
497 */
498int xtc_proc_recovery_track_fd(int fd);
499int xtc_proc_recovery_track_mctx(struct xtc_mctx *mctx);
500int xtc_proc_recovery_track_locks(void *mgr, uint64_t locker,
501 void (*release_all)(void *, uint64_t));
502int xtc_proc_recovery_track(void (*fn)(void *), void *arg);
503int xtc_proc_recovery_untrack_fd(int fd);
504void xtc_proc_recovery_cleanup(void);
505
506/*
507 * Arm a recovery frame whose default recovered action is to release
508 * all tracked resources and exit the proc with the fault code. On the
509 * normal (arming) pass it returns 0 and execution continues into the
510 * session work; on a contained fault it does NOT return -- it runs
511 * xtc_proc_recovery_cleanup() then xtc_exit_self(sig). Use this when
512 * the registered resources are the whole cleanup story; use the bare
513 * xtc_proc_recovery_arm() + a custom block when they are not.
514 */
515#define xtc_proc_recovery_arm_clean() \
516 do { \
517 int __xtc_rsig = xtc_proc_recovery_arm(); \
518 if (__xtc_rsig != 0) { \
519 xtc_proc_recovery_cleanup(); \
520 (void)xtc_exit_self(__xtc_rsig); \
521 } \
522 } while (0)
523
524/* Decode a DOWN signal (delivered to a monitor when its target exits)
525 * into the target pid and exit reason, without hand-rolling the
526 * on-wire layout. The DOWN/EXIT signals are sent packed; a mismatched
527 * (unpacked) mirror struct misreads `reason`. Returns XTC_OK if msg
528 * is a DOWN, XTC_E_INVAL otherwise. out_pid / out_reason may be NULL. */
529int xtc_down_decode(const void *msg, size_t len,
530 xtc_pid_t *out_pid, int *out_reason);
531
532/*
533 * Self-describing DOWN classification (requested by embedders whose
534 * app exit codes and signal numbers would otherwise share the single
535 * `reason` integer: a bare xtc_exit_self(1) was indistinguishable from
536 * a signal-1 (SIGHUP) contained fault). xtc_down_decode_ex fills an
537 * xtc_down_info_t whose `kind` says HOW the target ended, with the
538 * signal number and the app exit code in SEPARATE fields that never
539 * collide. The legacy single-integer xtc_down_decode still works and
540 * returns the same `reason` it always did.
541 */
542typedef enum {
543 XTC_DOWN_KIND_CLEAN = 0, /* target returned or xtc_exit_self(0) */
544 XTC_DOWN_KIND_EXIT = 1, /* xtc_exit_self(code), code in .exit_code */
545 XTC_DOWN_KIND_SIGNAL = 2, /* R1 contained fault, signal in .signal */
546 XTC_DOWN_KIND_NOPROC = 3, /* monitor raced a dead target (benign) */
547 XTC_DOWN_KIND_NOCONNECTION = 4 /* the cross-process channel died
548 * before a clean exit was seen
549 * (xtc_xproc: the fork'd child's
550 * control socket closed) */
551} xtc_down_kind_t;
552
553typedef struct {
554 xtc_pid_t pid; /* the target that went DOWN */
555 xtc_down_kind_t kind; /* how it ended (never ambiguous) */
556 int signal; /* signal number iff kind == SIGNAL, else 0 */
557 int exit_code; /* app code iff kind == EXIT, else 0 */
558 int reason; /* the legacy xtc_down_decode reason value */
559 uint64_t ref; /* the monitor reference (0 for a link EXIT) */
561
562/*
563 * Decode a DOWN or EXIT signal into a fully-classified xtc_down_info_t.
564 * Accepts both the monitor DOWN ('D') and the link EXIT ('E') signal
565 * shapes. Returns XTC_OK on either, XTC_E_INVAL for any other message.
566 * A monitor need no longer know the producer's encoding convention:
567 * kind + signal + exit_code are unambiguous by construction.
568 *
569 * PUBLIC: int xtc_down_decode_ex __P((const void *, size_t, xtc_down_info_t *));
570 */
571int xtc_down_decode_ex(const void *msg, size_t len,
572 xtc_down_info_t *out);
573
574/* Predicate + accessor helpers over a legacy `reason` integer, for
575 * callers that keep using xtc_down_decode. A signal-N fault and a
576 * bare xtc_exit_self(N) are STILL not distinguishable from `reason`
577 * alone (that is the whole reason to prefer xtc_down_decode_ex); these
578 * helpers only classify the two unambiguous sentinels. */
579static inline int xtc_down_is_signal_reason(int reason)
580{
581 return reason >= 1 && reason <= 255;
582}
583static inline int xtc_down_is_exit_reason(int reason)
584{
585 return reason == 0 || reason >= 256;
586}
587
588/* Internal: save / restore the current-proc context across a yield
589 * done by a lower-level primitive (e.g. xtc_amutex parking the
590 * fiber), so the proc still sees itself on resume. Opaque to the
591 * caller. */
592void *__xtc_proc_ctx_save(void);
593void __xtc_proc_ctx_restore(void *ctx);
594
595#endif /* XTC_PROC_H */