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