DEV Community

Cover image for Finite States Machines (in C)
Remo Dentato
Remo Dentato

Posted on • Edited on

Finite States Machines (in C)

Most of us learn to write state machines in C with a big while + switch and a tangle of breaks. 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); }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 the fsm { … } 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
Enter fullscreen mode Exit fullscreen mode

Under the hood:

  • fsm expands to an if (1) { goto fsm_state_START; fsm_exit: ; } else { … }.
    Execution jumps directly to START. A shared fsm_exit label is defined.

  • Each fsmstate(NAME) expands to for(;;) 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 explicit fsmexit 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 before fsm 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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Comment stripper diagram


Getting started

  1. Browse or clone the repository: repo.dentato.com
  2. Drop dist/fsm.h into your project and #include it.
  3. Define one fsm per function, with exactly one START state.
  4. Keep only states inside the fsm { … } block; transition with fsmgoto(State); leave with fsmexit.

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)