libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_osproc.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License,
4 * a copy of which is in the file LICENSE in the top-level directory
5 * of this distribution.
6 *
7 * src/inc/xtc_osproc.h
8 * OS-process spawn + control socket + lifecycle (R3).
9 *
10 * This is the "default-to-fork extension tier" backbone: a way to
11 * run work in a separate OS process -- either an exec'd program or
12 * a forked callback -- with a control channel back to the spawning
13 * loop and non-blocking lifecycle management (signal, reap, wait).
14 * Distinct from xtc_proc_spawn, which spawns an in-process fiber.
15 * Use this tier for un-cooperative or untrusted work that must not
16 * run in a loop thread (no in-thread forcible unwind exists), and
17 * for the classic one-backend-per-connection isolation model.
18 *
19 * PLATFORM: POSIX only (fork/exec + pidfd). On Windows every entry
20 * point is present but returns XTC_E_NOSYS (CreateProcess + a
21 * control pipe is a future port); callers must handle the decline.
22 *
23 * Control socket
24 * When opts.ctrl_socket is set, spawn creates an AF_UNIX
25 * SOCK_STREAM socketpair. The PARENT end is returned by
26 * xtc_osproc_ctrl_fd(): it is O_NONBLOCK and O_CLOEXEC and is
27 * pollable with xtc_proc_wait_fd / wrappable with xtc_net, so the
28 * spawning fiber can exchange control messages (framed via
29 * xtc_net_send_frame/recv_frame) without blocking its loop. The
30 * CHILD end is NOT close-on-exec: a forked callback receives it as
31 * the fn(ctrl_fd, ...) argument; an exec'd program inherits it and
32 * finds its number in the environment variable XTC_CTRL_FD.
33 *
34 * Exit notification
35 * On Linux (>= 5.3) spawn opens a pidfd, so xtc_osproc_wait parks
36 * the caller on a readable fd and reaps in O(1) with no SIGCHLD
37 * handler. On platforms without pidfd, wait falls back to a
38 * cooperative waitpid(WNOHANG) poll (it yields/sleeps between
39 * probes, so it never blocks the loop) -- a kqueue EVFILT_PROC
40 * fast path is a future optimisation. Either way the library
41 * installs no process-wide SIGCHLD handler, so it never collides
42 * with an embedder's own signal handling.
43 *
44 * fork() in a threaded process
45 * An xtc executor has multiple loop + offload-pool threads. fork
46 * duplicates only the calling thread; locks held by other threads
47 * are frozen in the child. The exec path is therefore safe (the
48 * child only does async-signal-safe work, then execs a fresh
49 * image). The fn (fork-only) path runs the callback in that
50 * half-initialised child: the callback must restrict itself to
51 * async-signal-safe work until it re-initialises its own runtime.
52 */
53
54#ifndef XTC_OSPROC_H
55#define XTC_OSPROC_H
56
57#include "xtc_export.h"
58
59#include "xtc.h"
60
61#include <stdint.h>
62
63#ifdef __cplusplus
64extern "C" {
65#endif
66
67typedef struct xtc_osproc xtc_osproc_t;
68
69typedef struct xtc_osproc_opts {
70 const char *name; /* label for logs/debug; may be NULL */
71
72 /* Exactly one of argv / fn must be set.
73 *
74 * argv != NULL: the child execvp(argv[0], argv)s a program.
75 * fn != NULL: the child runs fn(ctrl_fd, arg) and _exit()s with
76 * its return value (0..255). ctrl_fd is the child's
77 * control-socket end, or -1 if ctrl_socket == 0. */
78 char *const *argv;
79 int (*fn)(int ctrl_fd, void *arg);
80 void *arg;
81
82 int ctrl_socket; /* 1: create the control socketpair */
84
85/*
86 * Spawn an OS process. Callable from a loop fiber; never blocks the
87 * loop. Returns XTC_OK and *out on success; XTC_E_INVAL (bad opts:
88 * neither or both of argv/fn), XTC_E_NOMEM, or XTC_E_INTERNAL
89 * (fork/socketpair/exec setup failed).
90 *
91 * PUBLIC: int xtc_osproc_spawn __P((const xtc_osproc_opts_t *, xtc_osproc_t **));
92 */
93XTC_API int xtc_osproc_spawn(const xtc_osproc_opts_t *opts, xtc_osproc_t **out);
94
95/* The child's OS process id, or -1.
96 * PUBLIC: long xtc_osproc_pid __P((const xtc_osproc_t *)); */
97XTC_API long xtc_osproc_pid(const xtc_osproc_t *p);
98
99/* The parent end of the control socket (O_NONBLOCK, O_CLOEXEC), or -1
100 * if no control socket was requested. Owned by the handle; do not
101 * close it directly -- xtc_osproc_destroy does.
102 * PUBLIC: int xtc_osproc_ctrl_fd __P((const xtc_osproc_t *)); */
103XTC_API int xtc_osproc_ctrl_fd(const xtc_osproc_t *p);
104
105/* Send signal `sig` to the child (e.g. SIGTERM graceful, SIGKILL hard,
106 * SIGINT cancel). Returns XTC_OK, XTC_E_INVAL, or XTC_E_INTERNAL.
107 * PUBLIC: int xtc_osproc_signal __P((const xtc_osproc_t *, int)); */
108XTC_API int xtc_osproc_signal(const xtc_osproc_t *p, int sig);
109
110/* Non-blocking reap. If the child has exited, returns XTC_OK and (when
111 * status != NULL) stores the raw waitpid status; if still running,
112 * returns XTC_E_AGAIN. After XTC_OK the child is reaped (no zombie).
113 * PUBLIC: int xtc_osproc_try_wait __P((xtc_osproc_t *, int *)); */
114XTC_API int xtc_osproc_try_wait(xtc_osproc_t *p, int *status);
115
116/* Cooperative wait: park the calling fiber until the child exits (or
117 * timeout_ns elapses; < 0 = forever), reap it, and store the raw
118 * waitpid status in *status. Never blocks the loop thread. Returns
119 * XTC_OK (exited), XTC_E_AGAIN (timeout), or XTC_E_*. Must be called
120 * from a loop fiber (it parks); off a fiber it polls-and-sleeps.
121 * PUBLIC: int xtc_osproc_wait __P((xtc_osproc_t *, int *, int64_t)); */
122XTC_API int xtc_osproc_wait(xtc_osproc_t *p, int *status, int64_t timeout_ns);
123
124/* Close the control socket + pidfd and free the handle. Does NOT kill
125 * or reap a still-running child (signal + wait first for a clean
126 * shutdown); if the child already exited it is reaped here so it does
127 * not linger as a zombie.
128 * PUBLIC: void xtc_osproc_destroy __P((xtc_osproc_t *)); */
129XTC_API void xtc_osproc_destroy(xtc_osproc_t *p);
130
131/* ---- isolated-worker ergonomics ------------------------------------
132 *
133 * The request/reply convenience over the control socket: real MMU
134 * isolation (a separate OS process) with a simple framed call. The
135 * parent spawns a worker, then xtc_osproc_call() ships a request frame
136 * and parks for the reply (never blocking the loop). The child runs
137 * xtc_osproc_serve(), which loops handling framed requests until the
138 * socket closes.
139 */
140
141/* Spawn a forked isolated worker running fn(ctrl_fd, arg), with a
142 * control socket always created. Equivalent to xtc_osproc_spawn with
143 * opts{.name, .fn, .arg, .ctrl_socket=1}.
144 *
145 * PUBLIC: int xtc_osproc_isolated_spawn __P((const char *, int (*)(int, void *), void *, xtc_osproc_t **));
146 */
147XTC_API int xtc_osproc_isolated_spawn(const char *name,
148 int (*fn)(int ctrl_fd, void *arg), void *arg,
149 xtc_osproc_t **out);
150
151/* Parent side: send a request frame to the worker and park for its
152 * reply frame (allocated with the xtc allocator; free with xtc_free).
153 * max_reply caps the reply (0 = uncapped). Returns XTC_OK, XTC_E_AGAIN
154 * on timeout, or XTC_E_*. Requires a control socket.
155 *
156 * PUBLIC: int xtc_osproc_call __P((xtc_osproc_t *, const void *, size_t, void **, size_t *, size_t, int64_t));
157 */
158XTC_API int xtc_osproc_call(xtc_osproc_t *p, const void *req, size_t req_len,
159 void **reply, size_t *reply_len, size_t max_reply,
160 int64_t timeout_ns);
161
162/* Child-side handler: given a request, produce a reply. Set *reply
163 * (malloc'd, or NULL for an empty reply) and *reply_len; return XTC_OK
164 * to send it and keep serving, or non-XTC_OK to stop. xtc_osproc_serve
165 * frees *reply after sending. */
166typedef int (*xtc_osproc_handler_fn)(const void *req, size_t req_len,
167 void **reply, size_t *reply_len,
168 void *arg);
169
170/* Child side: serve framed requests on ctrl_fd until the peer closes
171 * (or a handler stops it). Returns the handler's stop code, or XTC_OK
172 * on clean EOF.
173 *
174 * PUBLIC: int xtc_osproc_serve __P((int, xtc_osproc_handler_fn, void *));
175 */
176XTC_API int xtc_osproc_serve(int ctrl_fd, xtc_osproc_handler_fn handler, void *arg);
177
178#ifdef __cplusplus
179}
180#endif
181
182#endif /* XTC_OSPROC_H */