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 <stddef.h>
42#include <stdint.h>
43
44#include "xtc.h"
45#include "xtc_loop.h"
46#include "xtc_proc.h"
47
48/*
49 * What the event() callback asks the machine to do next:
50 *
51 * XTC_FSM_KEEP stay in the current state (state data may have
52 * changed). A state_timeout_ns > 0 (re)arms the
53 * state timeout in the current state.
54 * XTC_FSM_NEXT transition to result.next_state. enter() runs
55 * for the new state, then any postponed events are
56 * replayed oldest first. state_timeout_ns > 0 arms
57 * a timeout for the new state.
58 * XTC_FSM_POSTPONE stash this event and re-deliver it after the next
59 * NEXT transition, ahead of fresh events. The
60 * machine stays in the current state.
61 * XTC_FSM_STOP run terminate() and exit the proc.
62 */
63typedef enum xtc_fsm_action {
64 XTC_FSM_KEEP = 0,
65 XTC_FSM_NEXT = 1,
66 XTC_FSM_POSTPONE = 2,
67 XTC_FSM_STOP = 3
68} xtc_fsm_action_t;
69
70typedef struct xtc_fsm_result {
71 xtc_fsm_action_t action;
72 int next_state; /* used iff action == XTC_FSM_NEXT */
73 int64_t state_timeout_ns; /* 0 = none; > 0 arms a state
74 * timeout for the resulting state */
76
77typedef struct xtc_fsm xtc_fsm_t;
78typedef struct xtc_fsm_call xtc_fsm_call_t;
79
80/*
81 * A synthetic state-timeout event is delivered to event() with
82 * msg == NULL and len == 0. A machine that uses state timeouts must
83 * treat that shape as its timeout signal.
84 */
85#define XTC_FSM_IS_STATE_TIMEOUT(msg, len) ((msg) == NULL && (len) == 0)
86
87/*
88 * Reason codes passed to terminate(). A clean XTC_FSM_STOP uses
89 * XTC_FSM_REASON_NORMAL; the machine may pass any nonzero reason of
90 * its own by storing it before returning XTC_FSM_STOP (the reason
91 * argument is currently always XTC_FSM_REASON_NORMAL for a graceful
92 * stop and nonzero if the loop is torn down under it).
93 */
94#define XTC_FSM_REASON_NORMAL 0
95
96typedef struct xtc_fsm_callbacks {
97 /* Called on every state change (old_state -> new_state), including
98 * the initial entry (old_state == new_state == initial_state).
99 * Returns XTC_OK to proceed; any XTC_E_* stops the machine.
100 * OK to be NULL. */
101 int (*enter)(void *state, int old_state, int new_state);
102
103 /* Called for each event in the current state. For a synchronous
104 * xtc_fsm_call, `call` is non-NULL and the callback must satisfy
105 * it exactly once with xtc_fsm_reply; for an asynchronous
106 * xtc_fsm_send (or a synthetic state timeout), `call` is NULL.
107 * Required. */
108 xtc_fsm_result_t (*event)(void *state, int cur_state,
109 const void *msg, size_t len,
110 xtc_fsm_call_t *call);
111
112 /* Called once when the machine stops. OK to be NULL. */
113 void (*terminate)(void *state, int reason);
115
116typedef struct xtc_fsm_opts {
117 const char *name; /* optional, for logs */
118 size_t mailbox_cap; /* 0 = default */
120
121/*
122 * PUBLIC: int xtc_fsm_start __P((xtc_loop_t *, const xtc_fsm_callbacks_t *, void *, int, const xtc_fsm_opts_t *, xtc_fsm_t **));
123 * PUBLIC: int xtc_fsm_stop __P((xtc_fsm_t *));
124 * PUBLIC: int xtc_fsm_join __P((xtc_fsm_t *, int64_t));
125 * PUBLIC: xtc_pid_t xtc_fsm_pid __P((const xtc_fsm_t *));
126 * PUBLIC: int xtc_fsm_send __P((xtc_pid_t, const void *, size_t));
127 * PUBLIC: int xtc_fsm_call __P((xtc_pid_t, const void *, size_t, void **, size_t *, int64_t));
128 * PUBLIC: int xtc_fsm_reply __P((xtc_fsm_call_t *, const void *, size_t));
129 */
130
131/*
132 * Start a state machine on `loop`. It runs as its own xtc_proc,
133 * beginning in `initial_state` (for which enter() is called before the
134 * first event). The returned handle lets a caller query/stop/join it
135 * from outside.
136 */
137int xtc_fsm_start(xtc_loop_t *loop,
138 const xtc_fsm_callbacks_t *cb,
139 void *state,
140 int initial_state,
141 const xtc_fsm_opts_t *opts,
142 xtc_fsm_t **out);
143
144/* Ask the machine to stop. Non-blocking; sets a flag and kicks the
145 * proc's recv. terminate() runs, then the proc exits. Safe from any
146 * thread, multiple times. */
147int xtc_fsm_stop(xtc_fsm_t *fsm);
148
149/* Wait for the machine to exit, then free its handle. timeout_ns < 0
150 * waits forever, 0 polls once. On XTC_OK the handle is invalid; on
151 * XTC_E_AGAIN (timeout) it is left intact so the caller may join
152 * again. */
153int xtc_fsm_join(xtc_fsm_t *fsm, int64_t timeout_ns);
154
155/* The machine's pid, for use with xtc_fsm_send / xtc_fsm_call. */
156xtc_pid_t xtc_fsm_pid(const xtc_fsm_t *fsm);
157
158/* Asynchronous event: deliver `ev` to the machine and return without
159 * waiting. The event lands in event() with call == NULL. */
160int xtc_fsm_send(xtc_pid_t target, const void *ev, size_t len);
161
162/* Synchronous event: deliver `req`, block until the machine replies or
163 * timeout_ns elapses. On XTC_OK, *out_reply / *out_size receive a
164 * heap buffer the caller must xtc_free. Returns XTC_E_AGAIN on
165 * timeout, XTC_E_INVAL on bad arguments. */
166int xtc_fsm_call(xtc_pid_t target,
167 const void *req, size_t req_size,
168 void **out_reply, size_t *out_size,
169 int64_t timeout_ns);
170
171/* From inside event() when `call` is non-NULL, send the reply and
172 * release the call handle. Each call must be replied exactly once. */
173int xtc_fsm_reply(xtc_fsm_call_t *call,
174 const void *reply, size_t size);
175
176#endif /* XTC_FSM_H */