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