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