libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_io.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 *
6 * src/inc/xtc_io.h
7 * The L1 event-notification engine: register interest in fd
8 * readiness, poll for ready events, wake the poller from another
9 * thread.
10 *
11 * Exactly one backend is compiled in per binary (configure-time).
12 * M2 ships poll(2) and epoll; later milestones add io_uring,
13 * kqueue, IOCP, Solaris event ports, and AIX pollset.
14 *
15 * See M2_CLAIMS.md.
16 */
17
18#ifndef XTC_IO_H
19#define XTC_IO_H
20
21#include "xtc_export.h"
22
23#include <stddef.h>
24#include <stdint.h>
25
26typedef struct xtc_io xtc_io_t;
27
28/*
29 * Event flag bits. Stable across minor versions; new flags appear
30 * at higher bit positions only (PLAN.md (S)18).
31 */
32#define XTC_IO_READABLE 0x01u
33#define XTC_IO_WRITABLE 0x02u
34#define XTC_IO_HUP 0x04u
35#define XTC_IO_ERR 0x08u
36#define XTC_IO_WAKEUP 0x10u /* set on the synthetic event delivered
37 by xtc_io_wakeup; tag is NULL. */
38#define XTC_IO_AIO 0x20u /* set on an async file-I/O completion;
39 tag is the xtc_aio_t's tag. */
40
41typedef struct xtc_io_event {
42 void *tag; /* the value passed at registration; NULL on wakeups */
43 uint32_t flags; /* XTC_IO_* bitset */
44 /*
45 * The fd this readiness is FOR, or -1 when the event is not about a
46 * registered fd (a wakeup, or an AIO completion whose identity is
47 * the tag).
48 *
49 * Every backend already knows this at the point it fills the event
50 * (kqueue's kevent.ident, epoll's own bookkeeping, the uring fd
51 * record, ...) and it used to be DISCARDED here -- which forced the
52 * dispatcher to GUESS, by unregistering `task->park_fd` from the
53 * loop that happened to be polling. Both halves of that guess are
54 * wrong for a migratable fiber: it registered on the loop it was
55 * RUNNING on, which after a work-steal is not the dispatching loop,
56 * and by then park_fd may already name a different registration.
57 *
58 * The consequence was not cosmetic. A stale entry left in a peer
59 * loop's registry means SEVERAL loops watch the same open file;
60 * whichever polls first dispatches the readiness to ITS OWN tag,
61 * waking a task that is not the parker. Measured on FreeBSD: one fd
62 * number registered in ALL TWELVE kqueues of a 12-loop executor.
63 * Carrying the fd lets dispatch unregister exactly the registration
64 * that fired, so no guess is needed.
65 */
66 int fd;
68
69/*
70 * Async file I/O. A native completion-based file operation: io_uring
71 * submits IORING_OP_READ/WRITE/FSYNC; kqueue submits POSIX AIO with
72 * SIGEV_KEVENT and reaps EVFILT_AIO; IOCP uses overlapped reads/writes
73 * on the port -- in each case the completion wakes the tagged task. On
74 * a readiness-only backend (epoll, poll, select, event ports, AIX)
75 * xtc_io_aio_submit returns XTC_E_NOSYS and the caller offloads the op
76 * to the blocking pool instead, because a regular file is not pollable.
77 * See xtc_aio(3).
78 */
79enum {
80 XTC_AIO_PREAD = 0,
81 XTC_AIO_PWRITE = 1,
82 XTC_AIO_FSYNC = 2, /* full sync: data + metadata */
83 XTC_AIO_FDATASYNC = 3, /* data only (fsync on macOS, which lacks it) */
84 XTC_AIO_PREADV = 4, /* vectored read (iov/iovcnt) */
85 XTC_AIO_PWRITEV = 5 /* vectored write (iov/iovcnt) */
86};
87typedef struct xtc_aio {
88 size_t size; /* sizeof(xtc_aio_t): XTC_AIO_INIT. The library
89 * keeps this pointer until completion and reads
90 * and writes the struct IN PLACE, so it refuses
91 * (XTC_E_INVAL) a size that does not cover every
92 * field below; a field appended in a later minor
93 * is read only when .size covers it. */
94 int fd;
95 int op; /* XTC_AIO_* */
96 void *buf; /* read/write buffer (scalar ops; NULL for FSYNC/V) */
97 /* Vectored ops (PREADV / PWRITEV): iov points at the caller's
98 * iovec array (struct iovec *, kept opaque here to avoid a
99 * <sys/uio.h> dependency in this header) and iovcnt is its count.
100 * Zero/NULL for the scalar and sync ops. */
101 void *iov; /* const struct iovec * */
102 int64_t off; /* file offset (ignored for FSYNC) */
103 void *tag; /* woken on completion (an xtc_task_t *) */
104 uint32_t len; /* byte count (scalar ops) */
105 int iovcnt;
106 /*
107 * 0 until the completion is reaped. CONCURRENCY CONTRACT: on a
108 * native completion backend (io_uring / IOCP / kqueue-AIO / Solaris
109 * ports) the REAPING thread stores this and the PARKED FIBER reads it
110 * in its wake-recheck loop, so it is a cross-thread flag.
111 *
112 * It is deliberately a PLAIN int, not `_Atomic int`, and the library
113 * accesses it through the __os_atomic_{load,store}_i32 helpers in
114 * src/inc/os_atomic.h -- which exist precisely for this shape (atomic
115 * primitives applied to the ADDRESS of plain storage, giving the same
116 * ordering guarantees as an _Atomic-qualified type while leaving
117 * ordinary load/store usable on the single-threaded pre-publication
118 * init path, where xtc_io_aio_submit clears it).
119 *
120 * Why not `_Atomic int`: layout is identical either way (gcc and
121 * clang, x86-64), but `_Atomic` is not valid C++, and this header has
122 * no extern "C" guard of its own (xtc.h has one; xtc_io.h is also
123 * includable directly). g++ -std=c++17 compiles this header today
124 * and would fail outright with `'_Atomic' does not name a type`.
125 * Consumers allocate xtc_aio_t on the stack, so that is a source
126 * break for every C++ embedder, in exchange for nothing the helper
127 * accesses do not already provide. Do not "upgrade" it.
128 */
129 int done;
130 int32_t res; /* bytes transferred, or -errno */
131} xtc_aio_t;
132
133#define XTC_AIO_INIT { .size = sizeof(xtc_aio_t) }
134
135/*
136 * PUBLIC: int xtc_io_init __P((xtc_io_t **));
137 * PUBLIC: int xtc_io_fini __P((xtc_io_t *));
138 * PUBLIC: const char *xtc_io_backend_name __P((void));
139 * PUBLIC: int xtc_io_reg_fd __P((xtc_io_t *, int, uint32_t, void *));
140 * PUBLIC: int xtc_io_mod_fd __P((xtc_io_t *, int, uint32_t, void *));
141 * PUBLIC: int xtc_io_del_fd __P((xtc_io_t *, int));
142 * PUBLIC: int xtc_io_poll __P((xtc_io_t *, xtc_io_event_t *, int, int64_t, int *));
143 * PUBLIC: int xtc_io_aio_submit __P((xtc_io_t *, xtc_aio_t *));
144 * PUBLIC: int xtc_io_wakeup __P((xtc_io_t *));
145 *
146 * PUBLIC: void xtc_io_set_iowq_max_workers __P((unsigned, unsigned));
147 */
148
149/* Lifecycle. */
150XTC_API int xtc_io_init(xtc_io_t **out);
151XTC_API int xtc_io_fini(xtc_io_t *io);
152XTC_API const char *xtc_io_backend_name(void);
153
154/* Registration. */
155XTC_API int xtc_io_reg_fd(xtc_io_t *io, int fd, uint32_t interest, void *tag);
156XTC_API int xtc_io_mod_fd(xtc_io_t *io, int fd, uint32_t interest, void *tag);
157XTC_API int xtc_io_del_fd(xtc_io_t *io, int fd);
158
159/*
160 * xtc_io_poll --
161 * Wait for events. On return *n_out holds the number of events
162 * written (0..max).
163 *
164 * timeout_ns:
165 * == 0 -> non-blocking
166 * > 0 -> wait at most that many nanoseconds
167 * < 0 -> wait indefinitely (until an fd becomes ready or
168 * xtc_io_wakeup is called)
169 */
170XTC_API int xtc_io_poll(xtc_io_t *io, xtc_io_event_t *events, int max,
171 int64_t timeout_ns, int *n_out);
172
173/* Submit an async file op. XTC_OK if queued (the caller parks until
174 * the completion event with tag a->tag arrives, then reads a->res);
175 * XTC_E_NOSYS if the backend has no native file completion (caller
176 * should offload instead). */
177XTC_API int xtc_io_aio_submit(xtc_io_t *io, xtc_aio_t *a);
178
179/*
180 * xtc_io_wakeup --
181 * From any thread, cause the next (or in-flight) xtc_io_poll on
182 * this io to return. Safe to call concurrently from many threads;
183 * multiple wakeups before the next poll coalesce into one event.
184 */
185XTC_API int xtc_io_wakeup(xtc_io_t *io);
186
187/*
188 * Cap the per-ring io_uring io-wq kernel worker pool (Linux io_uring
189 * backend only; a no-op on other backends). See the implementation
190 * comment in io_uring.c: prevents N carriers on an M-core box from
191 * accumulating N*M idle io-wq kernel threads. Call before the
192 * executor/loop creates its rings.
193 */
194XTC_API void xtc_io_set_iowq_max_workers(unsigned bound,
195 unsigned unbound);
196
197#endif /* XTC_IO_H */