Most of us learn to write state machines in C with a big while
+ switch
and a tangle of break
s. It works—until it doesn’t: fall-through bugs, nested loops, and “what happens if I forget a break
here?”
Alternative methods use a transition table, possibly with pointers to functions encoding states. Or just plain code with if
and state variables to control the flow of execution.
The fsm
macro library takes a different, extremely simple approach: it turns each state into a labeled block and each transition into an explicit goto
. The result is readable, predictable control flow and straight-line, optimizer-friendly code—no runtime framework, no heap, no state tables, just C code.
The key benefit of this approach is that it maps one-to-one the code with state machine diagrams, meaning that it's very simple to code an FSM starting from a diagram and, conversely, derive the diagram from the code, with great improvement of maintainability. (see the example at the end of the post)
🗃️ Source & repository: repo.dentato.com/fsm
What it looks like
#include <ctype.h>
#include <stdio.h>
#include "fsm.h"
void classify(const char *s) {
size_t i = 0;
fsm {
fsmstate(START) {
if (!s[i]) fsmexit;
int c = (unsigned char)s[i++];
if (!isalpha(c)) fsmgoto(START);
if (strchr("AEIOUaeiou", c)) fsmgoto(VOWEL);
fsmgoto(CONSONANT);
}
fsmstate(VOWEL) { puts("vowel"); fsmgoto(START); }
fsmstate(CONSONANT) { puts("consonant"); fsmgoto(START); }
}
}
Four macros do the job:
-
fsm { … }
— open an FSM in a function (one per function). -
fsmstate(NAME) { … }
— define a state. -
fsmgoto(NAME);
— transition. -
fsmexit;
— stop the machine (continue after thefsm { … }
block).
How it works (and why it’s safe)
This is the code for the library (the commented version is in the repo)
#define fsm if (1) {goto fsm_state_START; fsm_exit: ;} else
#define fsmstate(s) for(;;) for(;;) if (1) goto fsm_exit; else fsm_state_ ## s:
#define fsmgoto(s) goto fsm_state_ ## s
#define fsmexit goto fsm_exit
Under the hood:
fsm
expands to anif (1) { goto fsm_state_START; fsm_exit: ; } else { … }
.
Execution jumps directly toSTART
. A sharedfsm_exit
label is defined.Each
fsmstate(NAME)
expands tofor(;;) for(;;) if (1) goto fsm_exit; else fsm_state_NAME:
followed by your block.Each macro is a single statement from the compiler's point of view (no issue with dangling
else
or missing;
)No double evaluation of arguments is possible.
That design guarantees:
- If control ever reaches the top of a state by accident, it exits the FSM.
- If your state falls off the end, the next loop tick forces an exit—no silent fall-through.
- A bare
break
/continue
inside a state exits the FSM (prefer explicitfsmexit
for clarity). - Only the code within the
fsmstate()
blocks will be executed.
Constraints you should know
- One FSM per function (labels like
fsm_exit
,fsm_state_*
are reserved). - Exactly one
START
state. - Only valid identifiers as state names.
- Don’t put executable code between states inside the
fsm { … }
block; it won’t run. - Be mindful of VLAs: never
goto
into a scope that introduces a VLA per C11 rules. Declare VLAs either beforefsm
or inside a state.
Why this beats the usual switch
- No hidden fall-through. You must say where you go next.
- Lean codegen. Compilers emit direct branches; there’s no framework cost nor the cost to access data in memory (transition tables).
- Readable control flow. Each state is a labeled block you can jump to and grep for. You can very easily reconstruct the FSM diagram just by inspecting the code (see the "comment stripper" example below).
-
Debugger-friendly. You literally step between
fsm_state_*
labels.
Where fsm
shines
- Tokenizers and scanners (comments, strings, numbers).
- Protocol and message parsers.
- Little interpreters and command dispatchers.
A tiny real-world sketch: comment stripper
This example strips comments from a C source file:
void strip_comment(FILE *in, FILE *out)
{
fsm {
int c;
fsmstate(START) {
fsmgoto(code);
}
fsmstate(code) {
if ((c = fgetc(in)) == EOF) fsmexit;
if (c == '/') fsmgoto(slash);
fsmgoto(code_char);
}
fsmstate(code_char) { // Doesn't read from in
fputc(c, out);
if (c == '"') fsmgoto(string);
if (c == '\'') fsmgoto(literal);
fsmgoto(code);
}
fsmstate(slash) {
if ((c = fgetc(in)) == EOF) fsmexit;
if (c == '/') fsmgoto(line_comment);
if (c == '*') fsmgoto(block_comment);
fputc('/',out);
fsmgoto(code_char);
}
fsmstate(string) {
if ((c = fgetc(in)) == EOF) fsmexit;
if ((c == '\\')) fsmgoto(escaped_str);
fputc(c,out);
if (c == '"') fsmgoto(code);
fsmgoto(string);
}
fsmstate(escaped_str) {
fputc(c,out);
if ((c = fgetc(in)) == EOF) fsmexit;
fputc(c,out);
fsmgoto(string);
}
fsmstate(literal) {
if ((c = fgetc(in)) == EOF) fsmexit;
if ((c == '\\')) fsmgoto(escaped_lit);
fputc(c,out);
if (c == '\'') fsmgoto(code);
fsmgoto(literal);
}
fsmstate(escaped_lit) {
fputc(c,out);
if ((c = fgetc(in)) == EOF) fsmexit;
fputc(c,out);
fsmgoto(literal);
}
fsmstate(line_comment) {
if ((c = fgetc(in)) == EOF) fsmexit;
if (c == '\n') fsmgoto(code);
fsmgoto(line_comment);
}
fsmstate(block_comment) {
if ((c = fgetc(in)) == EOF) fsmexit;
if (c == '*') fsmgoto(star);
fsmgoto(block_comment);
}
fsmstate(star) {
if ((c = fgetc(in)) == EOF) fsmexit;
if (c == '/') fsmgoto(code);
fsmgoto(block_comment);
}
}
}
Thanks to the explicit nature of fsm
states and transactions, I've been able to quickly generate a state diagram directly from the code using an LLM (ChatGPT, in this case) and then edit it for clarity. This wouldn't have been easy (or even possible) if the FSM had been a tangled mess of switch and state variables.
Getting started
- Browse or clone the repository: repo.dentato.com
- Drop
dist/fsm.h
into your project and#include
it. - Define one
fsm
per function, with exactly oneSTART
state. - Keep only states inside the
fsm { … }
block; transition withfsmgoto(State)
; leave withfsmexit
.
For deeper patterns (error funnels, re-entrancy, tracing, diagrams), see docs/fsm.md
.
Credits
I took the original idea for these macros from the May 1991 issue of the glorious "Computer Language" magazine. The article "Goto? Yes, Goto!" by Tim Cooper exposed me to the idea of using macros to discipline the use of goto
in implementing Finite State Machines. Those macros were simple, and I used them in some form or another on various occasions. Since then, my macromancy has improved, and this is the result, almost 35 years after I read that article on a train on my way home.
Top comments (0)