libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_fsm.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_fsm.h
7 * The L4 gen_statem: a finite state machine that runs as an
8 * xtc_proc. It is a disciplined loop over xtc_recv: each event
9 * is dispatched to the user's event() callback for the current
10 * state, and the callback's result drives the machine.
11 *
12 * Modeled on Erlang's gen_statem. The three subtle features
13 * gen_statem exists to standardize -- and that hand-rolled C
14 * switch machines almost always botch -- are all provided here:
15 *
16 * - state_enter: enter() is called on every state change
17 * (old_state -> new_state), so entry actions live in one
18 * place instead of being scattered across every transition.
19 *
20 * - postponed events: an event that cannot be handled in the
21 * current state is stashed (XTC_FSM_POSTPONE) and REPLAYED,
22 * oldest first, after the next successful state transition,
23 * ahead of any fresh event. This is the feature most
24 * hand-rolled FSMs get wrong.
25 *
26 * - state timeouts: a result may arm a per-state timeout
27 * (state_timeout_ns > 0). If it fires before any event
28 * arrives, a synthetic state-timeout event is delivered to
29 * event() with msg == NULL and len == 0. Any real event
30 * cancels a pending state timeout (gen_statem semantics).
31 *
32 * The machine is driven by two client entry points: xtc_fsm_send
33 * (asynchronous event, fire-and-forget) and xtc_fsm_call
34 * (synchronous request/reply, reusing the same reply machinery as
35 * xtc_svr). A call reply is sent by the event() callback via
36 * xtc_fsm_reply (from the reply handle it is given).
37 */
38
39#ifndef XTC_FSM_H
40#define XTC_FSM_H
41
42#include "xtc_export.h"
43
44#include <stddef.h>
45#include <stdint.h>
46
47#include "xtc.h"
48#include "xtc_loop.h"
49#include "xtc_proc.h"
50
51/*
52 * What the event() callback asks the machine to do next:
53 *
54 * XTC_FSM_KEEP stay in the current state (state data may have
55 * changed). A state_timeout_ns > 0 (re)arms the
56 * state timeout in the current state.
57 * XTC_FSM_NEXT transition to result.next_state. enter() runs
58 * for the new state, then any postponed events are
59 * replayed oldest first. state_timeout_ns > 0 arms
60 * a timeout for the new state.
61 * XTC_FSM_POSTPONE stash this event and re-deliver it after the next
62 * NEXT transition, ahead of fresh events. The
63 * machine stays in the current state.
64 * XTC_FSM_STOP run terminate() and exit the proc.
65 */
66typedef enum xtc_fsm_action {
67 XTC_FSM_KEEP = 0,
68 XTC_FSM_NEXT = 1,
69 XTC_FSM_POSTPONE = 2,
70 XTC_FSM_STOP = 3
71} xtc_fsm_action_t;
72
73typedef struct xtc_fsm_result {
74 xtc_fsm_action_t action;
75 int next_state; /* used iff action == XTC_FSM_NEXT */
76 int64_t state_timeout_ns; /* 0 = none; > 0 arms a state
77 * timeout for the resulting state */
79
80typedef struct xtc_fsm xtc_fsm_t;
81typedef struct xtc_fsm_call xtc_fsm_call_t;
82
83/*
84 * A synthetic state-timeout event is delivered to event() with
85 * msg == NULL and len == 0. A machine that uses state timeouts must
86 * treat that shape as its timeout signal.
87 */
88#define XTC_FSM_IS_STATE_TIMEOUT(msg, len) ((msg) == NULL && (len) == 0)
89
90/*
91 * Reason codes passed to terminate(). A clean XTC_FSM_STOP uses
92 * XTC_FSM_REASON_NORMAL; the machine may pass any nonzero reason of
93 * its own by storing it before returning XTC_FSM_STOP (the reason
94 * argument is currently always XTC_FSM_REASON_NORMAL for a graceful
95 * stop and nonzero if the loop is torn down under it).
96 */
97#define XTC_FSM_REASON_NORMAL 0
98
99typedef struct xtc_fsm_callbacks {
100 size_t size; /* sizeof(xtc_fsm_callbacks_t): XTC_FSM_CALLBACKS_INIT */
101
102 /* Called on every state change (old_state -> new_state), including
103 * the initial entry (old_state == new_state == initial_state).
104 * Returns XTC_OK to proceed; any XTC_E_* stops the machine.
105 * OK to be NULL. */
106 int (*enter)(void *state, int old_state, int new_state);
107
108 /* Called for each event in the current state. For a synchronous
109 * xtc_fsm_call, `call` is non-NULL and the callback must satisfy
110 * it exactly once with xtc_fsm_reply; for an asynchronous
111 * xtc_fsm_send (or a synthetic state timeout), `call` is NULL.
112 * Required. */
113 xtc_fsm_result_t (*event)(void *state, int cur_state,
114 const void *msg, size_t len,
115 xtc_fsm_call_t *call);
116
117 /* Called once when the machine stops. OK to be NULL. */
118 void (*terminate)(void *state, int reason);
120
121#define XTC_FSM_CALLBACKS_INIT { .size = sizeof(xtc_fsm_callbacks_t) }
122
123typedef struct xtc_fsm_opts {
124 size_t size; /* sizeof(xtc_fsm_opts_t): XTC_FSM_OPTS_INIT */
125 const char *name; /* optional, for logs */
126 size_t mailbox_cap; /* 0 = default */
128
129#define XTC_FSM_OPTS_INIT { .size = sizeof(xtc_fsm_opts_t) }
130
131/*
132 * PUBLIC: int xtc_fsm_start __P((xtc_loop_t *, const xtc_fsm_callbacks_t *, void *, int, const xtc_fsm_opts_t *, xtc_fsm_t **));
133 * PUBLIC: int xtc_fsm_stop __P((xtc_fsm_t *));
134 * PUBLIC: int xtc_fsm_join __P((xtc_fsm_t *, int64_t));
135 * PUBLIC: xtc_pid_t xtc_fsm_pid __P((const xtc_fsm_t *));
136 * PUBLIC: int xtc_fsm_send __P((xtc_pid_t, const void *, size_t));
137 * PUBLIC: int xtc_fsm_call __P((xtc_pid_t, const void *, size_t, void **, size_t *, int64_t));
138 * PUBLIC: int xtc_fsm_reply __P((xtc_fsm_call_t *, const void *, size_t));
139 */
140
141/*
142 * Start a state machine on `loop`. It runs as its own xtc_proc,
143 * beginning in `initial_state` (for which enter() is called before the
144 * first event). The returned handle lets a caller query/stop/join it
145 * from outside.
146 */
147XTC_API int xtc_fsm_start(xtc_loop_t *loop,
148 const xtc_fsm_callbacks_t *cb,
149 void *state,
150 int initial_state,
151 const xtc_fsm_opts_t *opts,
152 xtc_fsm_t **out);
153
154/* Ask the machine to stop. Non-blocking; sets a flag and kicks the
155 * proc's recv. terminate() runs, then the proc exits. Safe from any
156 * thread, multiple times. */
157XTC_API int xtc_fsm_stop(xtc_fsm_t *fsm);
158
159/* Wait for the machine to exit, then free its handle. timeout_ns < 0
160 * waits forever, 0 polls once. On XTC_OK the handle is invalid; on
161 * XTC_E_AGAIN (timeout) it is left intact so the caller may join
162 * again. The start loop must outlive the join: do not finalize it
163 * until xtc_fsm_join has returned XTC_OK. */
164XTC_API int xtc_fsm_join(xtc_fsm_t *fsm, int64_t timeout_ns);
165
166/* The machine's pid, for use with xtc_fsm_send / xtc_fsm_call. */
167XTC_API xtc_pid_t xtc_fsm_pid(const xtc_fsm_t *fsm);
168
169/* Asynchronous event: deliver `ev` to the machine and return without
170 * waiting. The event lands in event() with call == NULL. */
171XTC_API int xtc_fsm_send(xtc_pid_t target, const void *ev, size_t len);
172
173/* Synchronous event: deliver `req`, block until the machine replies or
174 * timeout_ns elapses. On XTC_OK, *out_reply / *out_size receive a
175 * heap buffer the caller must xtc_free. Returns XTC_E_AGAIN on
176 * timeout, XTC_E_INVAL on bad arguments. */
177XTC_API int xtc_fsm_call(xtc_pid_t target,
178 const void *req, size_t req_size,
179 void **out_reply, size_t *out_size,
180 int64_t timeout_ns);
181
182/* From inside event() when `call` is non-NULL, send the reply and
183 * release the call handle. Each call must be replied exactly once. */
184XTC_API int xtc_fsm_reply(xtc_fsm_call_t *call,
185 const void *reply, size_t size);
186
187#endif /* XTC_FSM_H */