A modern C++ library for building robust finite state machines with a fluent builder interface, thread-safety support, and memory-efficient string interning. This library was written with much support from several AI.
Features
- Fluent Builder API: Type-safe, self-documenting interface for FSM construction
- Thread Safety: Optional multi-threaded support via separate library variant
- Memory Efficient: String interning reduces memory footprint and improves performance
- RAII Design: Move-only semantics and clear ownership models
- Flexible Architecture: No event loop management - integrates into existing applications
- Dual Library Variants: Separate single-threaded and multi-threaded libraries for optimal performance and clear usage
Library Architecture
FSMgine provides two library variants to ensure clear thread-safety semantics:
- **
libFSMgine
**: Single-threaded variant with no synchronization overhead
- **
libFSMgineMT
**: Multi-threaded variant with full thread-safety using mutexes
Both libraries share the same API but have different runtime characteristics:
- The single-threaded variant (
FSMgine
) has no locking overhead and doesn't require pthread
- The multi-threaded variant (
FSMgineMT
) provides thread-safe operations at the cost of synchronization overhead
The libraries include:
StringInterner
singleton implementation for memory-efficient state name storage
- Core FSM functionality
- Thread synchronization primitives (in the MT variant only)
Important: You must link against the appropriate FSMgine library variant based on your threading requirements.
Installation
# Clone the repository
git clone https://github.com/HarryPehkonen/FSMgine.git
cd FSMgine
# Create build directory
mkdir build && cd build
# Configure with CMake (builds both library variants by default)
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
# Or configure to build only specific variants:
# Single-threaded only:
cmake .. -DFSMGINE_BUILD_SINGLETHREADED=ON -DFSMGINE_BUILD_MULTITHREADED=OFF
# Multi-threaded only:
cmake .. -DFSMGINE_BUILD_SINGLETHREADED=OFF -DFSMGINE_BUILD_MULTITHREADED=ON
# Build and install
make
sudo make install
After installation, you'll have:
/usr/local/lib/libFSMgine.{a,so}
- Single-threaded library
/usr/local/lib/libFSMgineMT.{a,so}
- Multi-threaded library
/usr/local/include/FSMgine/
- Shared headers
/usr/local/lib/cmake/FSMgine/
- CMake config for single-threaded
/usr/local/lib/cmake/FSMgineMT/
- CMake config for multi-threaded
Quick Start
The simplest way to use FSMgine is with an event-less FSM, where state transitions are controlled by external variables.
#include <iostream>
bool coin_inserted = false;
bool door_pushed = false;
.onEnter("LOCKED", [](const std::monostate& event) { std::cout << "🔒 Locked\n"; })
.onEnter("UNLOCKED", [](const std::monostate& event) { std::cout << "🔓 Unlocked\n"; })
.from("LOCKED")
.predicate([&](const std::monostate& event) { return coin_inserted; })
.action([&](const std::monostate& event) { coin_inserted = false; })
.to("UNLOCKED");
.from("UNLOCKED")
.predicate([&](const std::monostate& event) { return door_pushed; })
.action([&](const std::monostate& event) { door_pushed = false; })
.to("LOCKED");
coin_inserted = true;
door_pushed = true;
Main header file for the FSMgine library.
A high-performance finite state machine implementation.
Definition FSM.hpp:94
bool process(const TEvent &event)
Processes an event and potentially transitions to a new state.
Definition FSM.hpp:294
FSMBuilder< TEvent > get_builder()
Creates a builder for fluent FSM construction.
Definition FSM.hpp:227
void setInitialState(std::string_view state)
Sets the initial state of the FSM.
Definition FSM.hpp:232
Main namespace for the FSMgine library.
Definition FSM.hpp:23
Quick Start (Event-Driven)
For a more robust and scalable design, you can define specific events to drive the FSM. This avoids managing external state variables and is the recommended approach for most applications.
#include <iostream>
enum class TurnstileEvent { COIN_INSERTED, DOOR_PUSHED };
int main() {
.onEnter("LOCKED", [](const TurnstileEvent& triggeringEvent){ std::cout << "🔒 Locked\n"; })
.onEnter("UNLOCKED", [](const TurnstileEvent& triggeringEvent){ std::cout << "🔓 Unlocked\n"; });
.from("LOCKED")
.predicate([](const TurnstileEvent& e) { return e == TurnstileEvent::COIN_INSERTED; })
.to("UNLOCKED");
.from("UNLOCKED")
.predicate([](const TurnstileEvent& e) { return e == TurnstileEvent::DOOR_PUSHED; })
.to("LOCKED");
turnstile.
process(TurnstileEvent::COIN_INSERTED);
turnstile.
process(TurnstileEvent::DOOR_PUSHED);
}
Usage Patterns
Encapsulating the FSM in a Class
For larger applications, it's best practice to encapsulate the FSM and its related state within a class. This provides a clean public API and hides implementation details.
class Turnstile {
public:
Turnstile() {
fsm_.get_builder()
.from("LOCKED")
.predicate([this](const std::monostate& event) { return coin_inserted_; })
.action([this](const std::monostate& event) { coin_inserted_ = false; })
.to("UNLOCKED");
fsm_.get_builder()
.from("UNLOCKED")
.predicate([this](const std::monostate& event) { return door_pushed_; })
.action([this](const std::monostate& event) { door_pushed_ = false; })
.to("LOCKED");
fsm_.setInitialState("LOCKED");
}
void insertCoin() { coin_inserted_ = true; }
void pushDoor() { door_pushed_ = true; }
void update() { fsm_.process(); }
private:
bool coin_inserted_ = false;
bool door_pushed_ = false;
};
Transitions Without Predicates
If a transition should always occur (no condition), you can omit the predicate:
.from("STATE_A")
.action([](const EventType& e) { })
.to("STATE_B");
State Management
FSMgine provides two methods for setting the current state:
- **
setInitialState(state)
**: Use this for first-time FSM initialization. It sets the current state and executes any onEnter
actions for that state. This should be called once after building your FSM to establish the starting state.
- **
setCurrentState(state)
**: Use this for runtime state changes when you need to forcibly change the state outside of normal transitions. It executes onExit
actions for the current state (if any) and onEnter
actions for the new state. This is useful for reset functionality or error recovery scenarios.
Example Use Cases
These examples demonstrate how to apply FSMgine to solve common problems. They illustrate patterns for managing state and logic within the FSM's actions and predicates.
1. Resource Pool Management
Demonstrates a thread-safe resource pool.
- Pattern: The FSM models the state of the pool (
IDLE
, BUSY
, EMPTY
). FSM actions modify an std::atomic<int>
counter for available resources. This example should be linked against FSMgineMT
to ensure thread-safe FSM operations in combination with atomic variables for concurrent access safety.
2. Protocol Parser
Shows how to build a state machine for parsing a simple network protocol string.
- Pattern: The FSM transitions character by character through states like
WAITING_HEADER
, READING_PAYLOAD
, and VALIDATING
. Actions append characters to buffer strings (current_command
, current_param
). This pattern is ideal for stream processing and validation tasks.
3. Calculator Implementation
Implements a calculator using two FSMs: one for tokenizing the input string and another for parsing and evaluating the expression.
- Pattern: Shows a more advanced, two-stage FSM design. The tokenizer FSM produces a stream of
Token
objects, which are then fed as events into the parser FSM. Actions in the parser manipulate stacks for values and operators, demonstrating how to maintain complex context between states.
4. Parentheses Checker
A simple but effective example that validates balanced parentheses in a string.
- Pattern: An FSM processes the input character by character. An
onEnter
action pushes opening parentheses onto a std::stack
, while other transitions pop and validate closing parentheses. This is a minimal but complete example of using an FSM for validation logic.
Integration
CMake Integration
FSMgine provides two library variants that can be used based on your threading requirements:
Single-Threaded Usage
cmake_minimum_required(VERSION 3.20)
project(your_project)
find_package(FSMgine REQUIRED)
add_executable(your_project src/main.cpp)
target_link_libraries(your_project PRIVATE FSMgine::FSMgine)
Multi-Threaded Usage
cmake_minimum_required(VERSION 3.20)
project(your_project)
find_package(FSMgineMT REQUIRED)
add_executable(your_project src/main.cpp)
target_link_libraries(your_project PRIVATE FSMgine::FSMgineMT)
Note: The CMake targets automatically handle:
- Linking against the appropriate static library
- Including necessary headers
- Linking against
Threads::Threads
(for FSMgineMT only)
- Setting the
FSMGINE_MULTI_THREADED
compile definition (for FSMgineMT only)
Build Options
-DFSMGINE_BUILD_SINGLETHREADED=ON
: Build single-threaded library (default: ON)
-DFSMGINE_BUILD_MULTITHREADED=ON
: Build multi-threaded library (default: ON)
-DTEST_MULTITHREADED=ON
: Run tests with multi-threaded library (default: matches FSMGINE_BUILD_MULTITHREADED)
-DEXAMPLES_USE_MULTITHREADED=ON
: Build examples with multi-threaded library (default: matches FSMGINE_BUILD_MULTITHREADED)
-DBUILD_TESTING=OFF
: Skip building tests
-DBUILD_EXAMPLES=ON
: Build example programs
-DBUILD_DOCUMENTATION=ON
: Enable documentation generation target
Documentation
FSMgine uses Doxygen for API documentation. The documentation is automatically built and deployed to GitHub Pages when changes are pushed to the main branch.
Viewing Documentation Online
Visit the FSMgine Documentation to browse the latest API documentation.
Building Documentation Locally
To build the documentation locally:
# Install Doxygen (if not already installed)
sudo apt-get install doxygen graphviz # On Ubuntu/Debian
# or
brew install doxygen graphviz # On macOS
# Configure with documentation enabled
cmake .. -DBUILD_DOCUMENTATION=ON
# Build documentation
make docs
# Open in browser
open docs/html/index.html # On macOS
xdg-open docs/html/index.html # On Linux
The generated documentation includes:
- Complete API reference for all classes
- Usage examples and code snippets
- Module organization and relationships
- Class diagrams and inheritance graphs
Troubleshooting
Undefined references to <tt>StringInterner</tt>
If you see linker errors like:
undefined reference to `fsmgine::StringInterner::instance()'
undefined reference to `fsmgine::StringInterner::intern(...)'
This means you're not linking against the FSMgine library. Ensure:
- You've called
find_package(FSMgine REQUIRED)
or find_package(FSMgineMT REQUIRED)
in your CMakeLists.txt
- You've added the appropriate target to your link libraries:
FSMgine::FSMgine
for single-threaded
FSMgine::FSMgineMT
for multi-threaded
- The appropriate library is properly installed (
sudo make install
was successful)
Example fix:
# For single-threaded:
find_package(FSMgine REQUIRED)
target_link_libraries(your_target PRIVATE FSMgine::FSMgine)
# For multi-threaded:
find_package(FSMgineMT REQUIRED)
target_link_libraries(your_target PRIVATE FSMgine::FSMgineMT)
Thread-related linking errors
If you're using FSMgineMT but getting pthread-related errors:
- The FSMgineMT CMake configuration should automatically handle pthread linking
- If issues persist, ensure your system has pthread development headers installed
Mixing library variants
Important: Do not link both FSMgine and FSMgineMT in the same executable. Choose one based on your threading requirements:
- Use
FSMgine
for single-threaded applications (no synchronization overhead)
- Use
FSMgineMT
for multi-threaded applications (thread-safe operations)
FSMgine package not found
If CMake cannot find FSMgine or FSMgineMT:
- Ensure the libraries are installed:
sudo make install
from the FSMgine build directory
- Check that you built the variant you're trying to use
- Check installation prefix matches your system's CMake search paths
- Alternatively, specify the path manually:
find_package(FSMgine REQUIRED PATHS /path/to/fsmgine/install)
# or
find_package(FSMgineMT REQUIRED PATHS /path/to/fsmgine/install)
Requirements
- C++17 or later
- CMake 3.20+
- Google Test (optional, for testing)
- Threads library (required only for FSMgineMT variant)
License
Please see the LICENSE file. This code is released to the public domain. Specifics are in the file.