libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_tnt.h
1/*-
2 * Copyright (c) 2026, The XTC Project -- All rights reserved.
3 * @stability experimental
4 * Use of this source code is governed by the ISC License.
5 *
6 * src/inc/xtc_tnt.h
7 * tnt -- a Tina-faithful, stackless Isolate layer (L4) built on
8 * top of the libxtc concurrency runtime. See the Tina reference at
9 * github.com/pmbanugo/tina.
10 *
11 * The decisive architectural choice is to map
12 * a Shard to one long-lived libxtc proc per xtc_exec loop, NOT an
13 * Isolate to a proc. The shard proc owns a dense typed arena of
14 * stackless Isolate structs and runs Tina's dispatch loop:
15 *
16 * drain inbox
17 * -> collect reactor completions
18 * -> for each ready type, for each dispatchable slot (budgeted):
19 * call handler -> interpret the returned transition/effect
20 * (commit staged I/O, enqueue sends, set state).
21 *
22 * Handlers NEVER block; they return a transition. Isolates are
23 * stackless arena structs; only the shard is a fiber.
24 *
25 * This header is the entire public surface. All ambient ctx_*
26 * style calls (here named xtc_tnt_*) are valid only during an active
27 * handler invocation -- they resolve the current shard + turn
28 * frame from thread-local state, exactly as Tina's TinaContext.
29 */
30
31#ifndef XTC_TNT_H
32#define XTC_TNT_H
33
34#include <stddef.h>
35#include <stdint.h>
36
37#ifdef __cplusplus
38extern "C" {
39#endif
40
41/* ---- Handles ------------------------------------------------------
42 *
43 * A 64-bit generational handle, bit-identical to Tina's layout:
44 *
45 * +----------+------------+-------------+--------------+
46 * | shard_id | type_id | slot_index | generation |
47 * | (8 bit) | (8 bit) | (20 bit) | (28 bit) |
48 * +----------+------------+-------------+--------------+
49 *
50 * The generation is the key to safety: when an Isolate is torn down,
51 * its slot's generation counter increments, so any handle still
52 * referencing the old generation is recognisably stale. A send to a
53 * stale handle returns XTC_TNT_SEND_STALE_HANDLE rather than delivering to
54 * whatever new Isolate now occupies the slot.
55 */
56typedef uint64_t xtc_tnt_handle_t;
57
58#define XTC_TNT_HANDLE_NONE ((xtc_tnt_handle_t)0)
59
60#define XTC_TNT_SHARD_BITS 8
61#define XTC_TNT_TYPE_BITS 8
62#define XTC_TNT_SLOT_BITS 20
63#define XTC_TNT_GEN_BITS 28
64
65#define XTC_TNT_SLOT_MAX ((1u << XTC_TNT_SLOT_BITS) - 1u)
66#define XTC_TNT_GEN_MASK ((1u << XTC_TNT_GEN_BITS) - 1u)
67
68static inline xtc_tnt_handle_t
69xtc_tnt_handle_make(uint8_t shard, uint8_t type, uint32_t slot, uint32_t gen)
70{
71 return ((xtc_tnt_handle_t)shard << (XTC_TNT_TYPE_BITS + XTC_TNT_SLOT_BITS +
72 XTC_TNT_GEN_BITS)) |
73 ((xtc_tnt_handle_t)type << (XTC_TNT_SLOT_BITS + XTC_TNT_GEN_BITS)) |
74 ((xtc_tnt_handle_t)(slot & XTC_TNT_SLOT_MAX) << XTC_TNT_GEN_BITS) |
75 ((xtc_tnt_handle_t)(gen & XTC_TNT_GEN_MASK));
76}
77
78static inline uint8_t
79xtc_tnt_handle_shard(xtc_tnt_handle_t h)
80{
81 return (uint8_t)(h >> (XTC_TNT_TYPE_BITS + XTC_TNT_SLOT_BITS + XTC_TNT_GEN_BITS));
82}
83
84static inline uint8_t
85xtc_tnt_handle_type(xtc_tnt_handle_t h)
86{
87 return (uint8_t)((h >> (XTC_TNT_SLOT_BITS + XTC_TNT_GEN_BITS)) & 0xffu);
88}
89
90static inline uint32_t
91xtc_tnt_handle_slot(xtc_tnt_handle_t h)
92{
93 return (uint32_t)((h >> XTC_TNT_GEN_BITS) & XTC_TNT_SLOT_MAX);
94}
95
96static inline uint32_t
97xtc_tnt_handle_gen(xtc_tnt_handle_t h)
98{
99 return (uint32_t)(h & XTC_TNT_GEN_MASK);
100}
101
102/* ---- Transitions --------------------------------------------------
103 *
104 * A handler returns a transition -- a small value describing what the
105 * Isolate wants next, not how to schedule it. The scheduler (the
106 * shard) interprets it. This is the core abstraction that lets the
107 * effect interpreter be swapped for deterministic simulation later:
108 * the Isolate code is identical in production and in DST.
109 */
110typedef enum xtc_tnt_transition_kind {
111 XTC_TNT_DONE = 0, /* clean exit -- deallocate me */
112 XTC_TNT_YIELD, /* run me again next tick */
113 XTC_TNT_WAIT_MESSAGE, /* park until my mailbox has a message */
114 XTC_TNT_WAIT_IO, /* staged I/O via xtc_tnt_submit_io -- park on it */
115 XTC_TNT_CRASH /* voluntary failure -- "let it crash" */
116} xtc_tnt_transition_kind_t;
117
118/* Fault reasons mirror Tina's Isolate_Fault_Reason. */
119typedef enum xtc_tnt_fault_reason {
120 XTC_TNT_FAULT_NONE = 0,
121 XTC_TNT_FAULT_SPAWN_FAILED,
122 XTC_TNT_FAULT_UNIMPLEMENTED_TRANSITION,
123 XTC_TNT_FAULT_INIT_FAILED,
124 XTC_TNT_FAULT_CONTRACT_VIOLATION
125} xtc_tnt_fault_reason_t;
126
127typedef struct xtc_tnt_transition {
128 xtc_tnt_transition_kind_t kind;
129 xtc_tnt_fault_reason_t fault_reason;
131
132#define XTC_TNT_TRANSITION_DONE ((xtc_tnt_transition_t){ XTC_TNT_DONE, XTC_TNT_FAULT_NONE })
133#define XTC_TNT_TRANSITION_YIELD ((xtc_tnt_transition_t){ XTC_TNT_YIELD, XTC_TNT_FAULT_NONE })
134#define XTC_TNT_TRANSITION_WAIT_MESSAGE ((xtc_tnt_transition_t){ XTC_TNT_WAIT_MESSAGE, XTC_TNT_FAULT_NONE })
135#define XTC_TNT_TRANSITION_WAIT_IO ((xtc_tnt_transition_t){ XTC_TNT_WAIT_IO, XTC_TNT_FAULT_NONE })
136
137static inline xtc_tnt_transition_t
138xtc_tnt_transition_to_crash(xtc_tnt_fault_reason_t reason)
139{
140 xtc_tnt_transition_t t = { XTC_TNT_CRASH, reason };
141 return t;
142}
143
144/* ---- Messages -----------------------------------------------------
145 *
146 * Every message is a fixed-size envelope with a tag discriminant and a
147 * raw_union body: a user payload or an I/O completion. Switch on
148 * message->tag to determine which.
149 */
150
151/* Tag constants -- system tags are < XTC_TNT_USER_TAG_BASE; user tags
152 * must be >= XTC_TNT_USER_TAG_BASE (matching Tina's 0x0040 base). */
153#define XTC_TNT_USER_TAG_BASE 0x0040u
154
155/* I/O completion tags (delivered by the effect interpreter / reactor,
156 * never user-sendable). Values match Tina's IO_TAG_* constants. */
157#define XTC_TNT_IO_TAG_ACCEPT_COMPLETE 0x0012u
158#define XTC_TNT_IO_TAG_CONNECT_COMPLETE 0x0013u
159#define XTC_TNT_IO_TAG_SEND_COMPLETE 0x0014u
160#define XTC_TNT_IO_TAG_RECV_COMPLETE 0x0015u
161#define XTC_TNT_IO_TAG_CLOSE_COMPLETE 0x0018u
162
163/* System tags. */
164#define XTC_TNT_TAG_TIMER 0x0002u
165#define XTC_TNT_TAG_SHUTDOWN 0x0003u
166
167#define XTC_TNT_MAX_PAYLOAD_SIZE 96
168#define XTC_TNT_MAX_INIT_ARGS_SIZE 64
169
170typedef struct xtc_tnt_message {
171 uint16_t tag;
172 union {
173 /* User / system message body. payload is placed after an
174 * 8-byte aligned prefix (source + size + pad) so that, with
175 * the union itself 8-aligned, payload starts on an 8-byte
176 * boundary -- a u32/u64 in the payload loads aligned. */
177 struct {
178 xtc_tnt_handle_t source; /* offset 0 */
179 uint16_t payload_size; /* offset 8 */
180 uint8_t _pad[6]; /* pad to 16 */
181 uint8_t payload[XTC_TNT_MAX_PAYLOAD_SIZE]; /* offset 16 */
182 } user;
183 /* I/O completion body. */
184 struct {
185 int fd; /* which fd completed (or new fd) */
186 int32_t result; /* bytes transferred, or -errno */
187 void *buffer; /* reactor buffer (recv only) */
188 uint32_t buffer_len;
189 } io;
190 } body;
192
193/* ---- Isolate type descriptor --------------------------------------
194 *
195 * Three artifacts per Isolate type (Tina's IsolateTypeDescriptor): the
196 * struct stride, an init_fn (called once on spawn, returns the initial
197 * transition), and a handler_fn (called on every message / completion,
198 * returns the next transition). Both functions receive a rawptr to
199 * the Isolate's slot because the scheduler operates on heterogeneous
200 * typed arenas; use xtc_tnt_self_as to cast.
201 */
202typedef xtc_tnt_transition_t (*xtc_tnt_init_fn)(void *self, const void *args,
203 size_t args_size);
204typedef xtc_tnt_transition_t (*xtc_tnt_handler_fn)(void *self, xtc_tnt_message_t *msg);
205
206typedef struct xtc_tnt_type {
207 uint8_t id; /* 0..255 -- index in spec.types */
208 const char *name; /* for diagnostics */
209 uint32_t slot_count; /* arena capacity for this type */
210 size_t stride; /* sizeof(the Isolate struct) */
211 size_t working_memory_size; /* per-slot working arena bytes */
212 uint32_t mailbox_capacity; /* bounded mailbox depth */
213 uint32_t budget_weight; /* max slots dispatched per tick */
214 xtc_tnt_init_fn init_fn;
215 xtc_tnt_handler_fn handler_fn;
217
218/* ---- System spec --------------------------------------------------
219 *
220 * Tina's SystemSpec, trimmed to this slice. At boot xtc_tnt_start carves
221 * every arena from boot-time slab caches (one per type per shard) and
222 * never mallocs on the hot path again (disciplinary, per the doc).
223 */
224typedef struct xtc_tnt_spec {
225 const char *name;
226 const xtc_tnt_type_t *types; /* descriptor table */
227 int n_types;
228 int shard_count; /* number of shards (loops) */
229 uint32_t scratch_size; /* per-shard turn scratch arena bytes */
230 uint32_t recv_buf_size;/* per-recv reactor buffer bytes */
231 /* Optional: a type id to auto-spawn one instance of on shard 0
232 * once the runtime is up (Tina's boot spec spawns a root). Set to
233 * a negative value (default after memset) to disable; the canonical
234 * use is a "driver" or "root supervisor" isolate. -1 = none. */
235 int boot_type;
237
238/* ---- Send results -------------------------------------------------
239 *
240 * Tina's Send_Result. The sender decides what to do -- the layer
241 * never auto-retries or grows the mailbox.
242 */
243typedef enum xtc_tnt_send_result {
244 XTC_TNT_SEND_OK = 0, /* enqueued in the target mailbox */
245 XTC_TNT_SEND_MAILBOX_FULL, /* target mailbox at capacity -- DROPPED */
246 XTC_TNT_SEND_POOL_EXHAUSTED, /* no free envelope -- DROPPED */
247 XTC_TNT_SEND_STALE_HANDLE /* target dead / generation mismatch */
248} xtc_tnt_send_result_t;
249
250/* Spawn outcome. */
251typedef enum xtc_tnt_spawn_error {
252 XTC_TNT_SPAWN_OK = 0,
253 XTC_TNT_SPAWN_ARENA_FULL,
254 XTC_TNT_SPAWN_TYPE_NOT_ALLOCATED,
255 XTC_TNT_SPAWN_INIT_FAILED
256} xtc_tnt_spawn_error_t;
257
258/* I/O submit results. */
259typedef enum xtc_tnt_io_result {
260 XTC_TNT_IO_OK = 0,
261 XTC_TNT_IO_TOO_MANY, /* turn frame staging slots exhausted */
262 XTC_TNT_IO_BAD_FD
263} xtc_tnt_io_result_t;
264
265/* ---- Ambient ctx_* API (valid only during a handler) -------------- */
266
267/* Messaging. tag must be >= XTC_TNT_USER_TAG_BASE. Payload max 96 bytes.
268 * Drop-on-full with sender feedback, matching Tina's .mailbox_full. */
269xtc_tnt_send_result_t xtc_tnt_send(xtc_tnt_handle_t to, uint16_t tag,
270 const void *payload, size_t payload_size);
271
272/* Spawn a new Isolate of type_id on the CURRENT shard. The child's
273 * init_fn runs immediately (not deferred). Returns the new handle in
274 * *out_handle on success. */
275xtc_tnt_spawn_error_t xtc_tnt_spawn(uint8_t type_id, const void *args,
276 size_t args_size, xtc_tnt_handle_t *out_handle);
277
278/* I/O effects. These stage an operation into the current turn frame;
279 * the shard commits it into xtc_aio / the net layer only if the
280 * handler returns XTC_TNT_WAIT_IO. This is Tina's stage-then-commit. */
281
282/* Stage a recv on fd into a reactor buffer; on completion the Isolate
283 * receives a XTC_TNT_IO_TAG_RECV_COMPLETE message. */
284xtc_tnt_io_result_t xtc_tnt_submit_recv(int fd);
285
286/* Stage a send of buffer[0..len) on fd; buffer must live inside the
287 * Isolate struct (it is read at commit time, after the handler
288 * returns). On completion: XTC_TNT_IO_TAG_SEND_COMPLETE. */
289xtc_tnt_io_result_t xtc_tnt_io_send(int fd, const void *buffer, size_t len);
290
291/* Zero-copy I/O. The reactor buffer of a RECV_COMPLETE message is
292 * freed when the handler returns; xtc_tnt_recv_take(msg), called in the
293 * handler, keeps it: it returns msg->body.io.buffer (NULL if msg is not
294 * a RECV_COMPLETE or has no buffer), clears it in msg, and the caller
295 * owns it from then on (release with xtc_free, or hand it to
296 * xtc_tnt_io_send_owned). xtc_tnt_io_send_owned stages a send like
297 * xtc_tnt_io_send of a buffer from libxtc's allocator (xtc_malloc, or
298 * one from xtc_tnt_recv_take) that need not live in the Isolate: on
299 * XTC_TNT_IO_OK the runtime owns it and frees it once the send has
300 * completed, or when the handler does not return XTC_TNT_WAIT_IO (the
301 * op is never committed); on any other result the caller still owns it.
302 * Echoing a received chunk is then recv_take + io_send_owned: no copy. */
303xtc_tnt_io_result_t xtc_tnt_io_send_owned(int fd, void *buffer, size_t len);
304void *xtc_tnt_recv_take(xtc_tnt_message_t *msg);
305
306/* Stage a close of fd; on completion: XTC_TNT_IO_TAG_CLOSE_COMPLETE. */
307xtc_tnt_io_result_t xtc_tnt_submit_close(int fd);
308
309/* Register a one-shot timer; after duration_ns the Isolate receives a
310 * message with the given tag. */
311void xtc_tnt_register_timer(uint64_t duration_ns, uint16_t tag);
312
313/* The current Isolate's identity. */
314xtc_tnt_handle_t xtc_tnt_self(void);
315
316/* The current shard's 0-based id. */
317uint8_t xtc_tnt_shard_id(void);
318
319/* The shard-wide scratch arena, reset before every handler call. For
320 * per-turn temporaries only; do not retain across handler returns.
321 * Returns a bump pointer of `size` bytes, or NULL if exhausted. */
322void *xtc_tnt_scratch_arena(size_t size);
323
324/* ---- Ergonomic helpers -------------------------------------------- */
325
326/* Debug-checked cast from the rawptr self to a typed Isolate pointer.
327 * In a debug build this validates the stride matches sizeof(T). */
328#define xtc_tnt_self_as(T, self_raw) ((T *)(self_raw))
329
330/* Cast a message payload to a typed pointer. */
331#define xtc_tnt_payload_as(T, msg) ((T *)((msg)->body.user.payload))
332
333/* ---- Boot ---------------------------------------------------------
334 *
335 * Validate the spec, carve all arenas from boot-time slab caches, pin
336 * one shard proc per loop, and enter the run loop. Blocks until the
337 * system stops (xtc_tnt_stop from any thread, or all shards idle). Returns
338 * XTC_OK-style 0 on clean shutdown, negative on a boot failure.
339 */
340int xtc_tnt_start(const xtc_tnt_spec_t *spec);
341
342/* Request the running system to stop (kicks every shard). Safe to
343 * call from a signal handler or another thread. */
344void xtc_tnt_stop(void);
345
346/* Spawn an Isolate from OUTSIDE the shard fibers (e.g. from main before
347 * xtc_tnt_start, or from a non-isolate listener). Routes the spawn onto
348 * the target shard and runs the init synchronously on that shard's next
349 * tick. shard is 0..shard_count-1. Thread-safe. */
350xtc_tnt_spawn_error_t xtc_tnt_spawn_on(uint8_t shard, uint8_t type_id,
351 const void *args, size_t args_size);
352
353#ifdef __cplusplus
354}
355#endif
356
357#endif /* XTC_TNT_H */