libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
io_int.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/io_int.h
6 * Internal struct definition for the L1 backend implementations.
7 */
8
9#ifndef XTC_IO_INT_H
10#define XTC_IO_INT_H
11
12#include "xtc_io.h"
13#include <stdatomic.h>
14
15#if defined(XTC_IO_BACKEND_EPOLL)
16/* nothing to predefine; epoll keeps tags via epoll_data_t */
17#elif defined(XTC_IO_BACKEND_KQUEUE)
18/* kqueue's EV_ADD is idempotent and EV_DELETE silently ignores absent
19 * filters, so we keep an authoritative side-table of currently-
20 * registered fds to enforce M2's semantic contract. */
21#elif defined(XTC_IO_BACKEND_SOLARIS)
22/* illumos event ports are one-shot per association, so we maintain
23 * the same side-table as kqueue and re-arm on each delivery. */
24struct __xtc_solaris_reg {
25 int fd;
26 uint32_t interest;
27 void *tag;
28};
29#elif defined(XTC_IO_BACKEND_IOCP)
30/*
31 * Windows IOCP backend (round 2): native completion port plus the
32 * AFD poll fast path that turns a connected/listening socket's
33 * readiness into an IOCP completion.
34 *
35 * Each socket registration owns one AFD poll OVERLAPPED that is
36 * armed (submitted via NtDeviceIoControlFile(IOCTL_AFD_POLL)) into
37 * the kernel and re-armed after every completion -- level-triggered
38 * emulation matching the epoll/kqueue contract.
39 *
40 * OVERLAPPED OWNERSHIP RULE (the classic IOCP correctness invariant,
41 * enforced throughout io_iocp.c):
42 * The OVERLAPPED (ov) and the AFD_POLL_INFO (poll_info) embedded in
43 * a registration belong to the KERNEL from the moment the AFD poll
44 * is armed (NtDeviceIoControlFile returns STATUS_PENDING) until the
45 * matching completion is dequeued from the port by
46 * GetQueuedCompletionStatusEx. While armed (pending == 1) neither
47 * buffer may be freed or reused. Deregistering an armed socket
48 * issues NtCancelIoFileEx and marks the registration dead; the
49 * storage is only released once the (possibly canceled) completion
50 * is reaped. This is why the struct embeds ov/poll_info by value
51 * and the registration node is freed lazily, never inline with
52 * xtc_io_del_fd while a poll is in flight.
53 *
54 * The OVERLAPPED is the FIRST member so a completion's
55 * OVERLAPPED_ENTRY.lpOverlapped pointer can be cast straight back to
56 * the owning registration. Each registration is a separately
57 * heap-allocated node with a STABLE address (held in a pointer array,
58 * never an inline array), because the kernel-owned OVERLAPPED carries
59 * a back-pointer to its node for the whole time the poll is armed --
60 * a realloc or swap-remove of an inline array would dangle it.
61 */
62struct __xtc_iocp_reg {
63 struct __xtc_iocp_overlapped *ovp; /* OVERLAPPED + AFD_POLL_INFO (heap) */
64 int fd; /* the SOCKET as an int (Winsock handle) */
65 void *base; /* base socket HANDLE (SIO_BASE_HANDLE) */
66 uint32_t interest;
67 void *tag;
68 int pending; /* 1 while an AFD poll is armed in the kernel */
69 int dead; /* deregistered; awaiting terminal completion */
70 int64_t armed_at_ns; /* __os_clock_mono() when this arm was issued;
71 * drives the bounded re-poll workaround for
72 * the AFD async-completion bug (see io_iocp.c
73 * __xtc_iocp_repoll_sweep) */
74};
75/* A file AIO (pread/pwrite) in flight on the IOCP backend. The
76 * file HANDLE is associated with the completion port, so the
77 * overlapped ReadFile/WriteFile completion is dequeued by
78 * GetQueuedCompletionStatusEx like any socket event -- no hEvent and
79 * no WaitForMultipleObjects. fsync has no async form on Windows
80 * (FlushFileBuffers is synchronous) and is offloaded. The OVERLAPPED
81 * is the FIRST member so the completion's lpOverlapped recovers the
82 * node; ownership follows the same kernel-owns-while-pending rule as
83 * the socket poll OVERLAPPED above. */
84struct __xtc_iocp_aio {
85 void *ov; /* OVERLAPPED * (heap; first field; owned here) */
86 void *aio; /* xtc_aio_t * awaiting completion */
87 void *fh; /* HANDLE: the file, for GetOverlappedResult */
88};
89#elif defined(XTC_IO_BACKEND_AIX)
90/* AIX pollset_* backend. Like solaris/kqueue, we maintain a
91 * side-table for duplicate detection and to map fd -> user tag
92 * (pollset itself doesn't carry udata). */
93struct __xtc_aix_reg {
94 int fd;
95 uint32_t interest;
96 void *tag;
97};
98#elif defined(XTC_IO_BACKEND_URING)
99#include <liburing.h>
100#include <pthread.h>
101/*
102 * Per-fd state for the io_uring backend. The user_data passed to
103 * each POLL_ADD points at one of these. The fd_table maps fd ->
104 * uring_fd so we can find/cancel a registration on _del/_mod.
105 */
106struct __xtc_uring_fd {
107 int fd;
108 uint32_t interest;
109 void *tag;
110 int is_wakeup; /* 1 for the internal wakeup pipe */
111 int dead; /* deleted; awaiting terminal CQE before free */
112 struct __xtc_uring_fd *next; /* free-list / fd-list linkage */
113};
114#elif defined(XTC_IO_BACKEND_POLL)
115#include <poll.h>
116#elif defined(XTC_IO_BACKEND_SELECT)
117#include <sys/select.h>
118#elif defined(XTC_IO_BACKEND_SIM)
119/* Deterministic-simulation backend (DST): no kernel poller. Readiness
120 * and file-AIO completions come from a scripted in-process event store
121 * driven against the virtual clock; the wakeup is an in-process flag.
122 * See src/io/io_sim.c. */
123#else
124# error "M2 build expects XTC_IO_BACKEND_{POLL,EPOLL,URING,KQUEUE,IOCP,SOLARIS,AIX,SELECT,SIM} to be defined"
125#endif
126
127struct xtc_io {
128 int wakeup_rfd;
129 int wakeup_wfd;
130
131#if defined(XTC_IO_BACKEND_EPOLL)
132 int epfd;
133#elif defined(XTC_IO_BACKEND_KQUEUE)
134 int epfd; /* kqueue fd */
135 int *reg_fds; /* registered fd list */
136 int n_reg;
137 int cap_reg;
138#elif defined(XTC_IO_BACKEND_SOLARIS)
139 int epfd; /* event-port fd */
140 struct __xtc_solaris_reg *reg_fds;
141 int n_reg;
142 int cap_reg;
143#elif defined(XTC_IO_BACKEND_IOCP)
144 void *iocp; /* HANDLE: the completion port */
145 void *afd; /* HANDLE: \Device\Afd, port-associated */
146 _Atomic int wakeup_pending; /* 1 = a wakeup completion is queued */
147 struct __xtc_iocp_reg **reg_iocp; /* live registration nodes (stable) */
148 int n_reg;
149 int cap_reg;
150 struct __xtc_iocp_reg **dead_iocp; /* deregistered, awaiting completion */
151 int n_dead;
152 int cap_dead;
153 struct __xtc_iocp_aio *aio_pend; /* file AIOs in flight */
154 int n_aio;
155 int cap_aio;
156#elif defined(XTC_IO_BACKEND_AIX)
157 int ps; /* pollset_t */
158 void *reg_aix; /* struct __xtc_aix_reg * */
159 int n_reg;
160 int cap_reg;
161#elif defined(XTC_IO_BACKEND_URING)
162 struct io_uring ring;
163 struct __xtc_uring_fd *fds;
164 struct __xtc_uring_fd *zombies; /* deleted fds awaiting terminal CQE */
165 /*
166 * Cross-loop deferred unregister queue. io->fds / io->zombies and
167 * the SQ ring are single-producer, owned by this io's loop thread.
168 * A migratable fiber that parked on this loop but was woken via a
169 * non-fd path (timeout / xtc_proc_wake / mailbox) and then work-
170 * stolen resumes on ANOTHER thread with its park_fd still live, and
171 * its xtc_proc_wait_fd cleanup would otherwise call xtc_io_del_fd on
172 * THIS io from the wrong thread -- racing the fd list AND the SQ ring
173 * (the native-path concurrent-commit collapse, TSan-caught
174 * 2026-08-30). Instead the foreign thread posts the fd here under
175 * del_lock and nudges this loop; the owning thread drains it at the
176 * top of xtc_io_poll and performs the real unregister on its own
177 * ring. del_lock guards ONLY this small queue, never the hot fds
178 * list or the ring. */
179 int *pending_del; /* fds awaiting owner-thread unregister */
180 int n_pending_del;
181 int cap_pending_del;
182 _Atomic int has_pending_del; /* fast, lock-free "is queue non-empty?" */
183 pthread_mutex_t del_lock; /* guards ONLY pending_del (not fds/ring) */
184 /*
185 * The exec-relative id of the loop that owns this io, purely as a
186 * LABEL for XTC_TAIL_REAP (which is emitted inside xtc_io_poll, where
187 * the reap outcome is visible but the loop is not). -1 until the
188 * owning loop publishes it; never used for control flow.
189 */
190 int tail_loop_id;
191 pthread_t owner_tid; /* the thread that polls this io */
192 _Atomic int owner_set; /* 1 once owner_tid is recorded */
193#if defined(XTC_DIAGNOSTIC)
194 /*
195 * DIAGNOSTIC-only: the thread that CREATED this ring, recorded in
196 * __xtc_io_backend_init.
197 *
198 * The ring is created by whoever calls xtc_loop_init -- for an
199 * executor that is the exec-init thread (xtc_exec_init), not the
200 * worker that will own and submit to it -- and the wakeup POLL_ADD
201 * is submitted right there on the creating thread
202 * (__xtc_io_register_wakeup). Every later submit comes from the
203 * owner. So one thread touches the SQ before another takes it over.
204 *
205 * That is BENIGN TODAY only because of an ORDERING that nothing
206 * enforces: creation completes before any worker starts, so the two
207 * touches never overlap. It is also the exact shape of eight fixed
208 * cross-loop defects (one thread touching a structure another owns),
209 * and it is the hard blocker for IORING_SETUP_SINGLE_ISSUER, which
210 * requires the creator and the submitter to be the same task.
211 *
212 * Rather than restructure loop init to chase that flag -- 88 of the
213 * 90 files calling xtc_loop_init also call xtc_loop_run on the SAME
214 * thread, so deferring ring creation would churn the most-used
215 * lifecycle in the tree for a gain the measured reap headroom says is
216 * noise -- record the creator and ASSERT the ordering holds. If a
217 * future change ever submits from the creator AFTER an owner is
218 * established, this fires immediately and locates it, instead of the
219 * property silently rotting.
220 */
221 pthread_t creator_tid;
222#endif
223 /*
224 * L2 ring-pointer preempt (INSPIRED BY Glommio's need_preempt():
225 * reactor.rs / sys/uring.rs preempt_pointers). A dedicated tiny
226 * ring carrying ONLY a rearmed TIMEOUT SQE, so
227 * io_uring_cq_ready(&preempt_ring) -- two relaxed/acquire loads of
228 * that ring's own head/tail, no syscall, no signal -- means "the
229 * preempt interval elapsed". A separate ring (not io->ring) is
230 * what makes the pointer test isolate the timeout: I/O CQEs on the
231 * main ring advance its head/tail unpredictably, but this ring sees
232 * only the timeout. preempt_armed gates it (0 = off, the ring is
233 * not initialised). */
234 struct io_uring preempt_ring;
235 int preempt_armed;
236 struct __kernel_timespec preempt_ts; /* the rearm interval */
237#elif defined(XTC_IO_BACKEND_POLL)
238 struct pollfd *pfds;
239 void **tags;
240 int n;
241 int cap;
242#elif defined(XTC_IO_BACKEND_SELECT)
243 /* Parallel fd[], interest[], tag[] arrays. fd_set is built
244 * each poll() call from these. Capped at FD_SETSIZE. */
245 int *fds;
246 uint32_t *interests;
247 void **tags;
248 int n;
249 int cap;
250#elif defined(XTC_IO_BACKEND_SIM)
251 /* Registered fds (tag map) for readiness simulation, a scripted
252 * event queue ordered by virtual-time due, and an in-process
253 * wakeup flag. Defined in io_sim.c; opaque here. */
254 struct __xtc_sim_io *sim;
255#endif
256};
257
258/*
259 * Cross-loop deferred fd-unregister. Each backend .c provides one:
260 * io_uring queues the fd and drains it on the owning thread (its fds
261 * list + SQ ring are single-owner); the other backends passthrough to
262 * xtc_io_del_fd (kernel-synchronized or their own registry). Internal;
263 * the sole caller is xtc_proc_wait_fd's post-migration cleanup.
264 */
265int __xtc_io_defer_del_fd(xtc_io_t *io, int fd);
266
267/*
268 * Label an io with the exec-relative id of the loop that owns it, for
269 * XTC_TAIL_REAP. No-op on backends that do not reap CQEs. Label only --
270 * never used for control flow.
271 */
272void __xtc_io_set_tail_loop_id(xtc_io_t *io, int id);
273
274#endif /* XTC_IO_INT_H */