typed-fsm
A lightweight, zero-cost, event-driven finite state machine microframework for Rust with ISR and concurrency support.
typed-fsm provides a declarative macro-based approach to building type-safe, event-driven state machines. Perfect for embedded systems with interrupt handlers, real-time applications, protocol implementations, and any scenario requiring robust state management with thread-safe or ISR-safe dispatch.
Features
Core
- Event-Driven Architecture - Built from the ground up for event-based systems
- ISR-Safe Dispatch - Call
dispatch()from interrupt service routines (optionalconcurrentfeature) - Thread-Safe Concurrency - Safe concurrent access from multiple threads with atomic protection
- Zero-cost abstraction - Compiles to efficient jump tables with no runtime overhead
- Type-safe - Compile-time validation of state transitions and events
- Declarative - Clean, readable syntax using macros
- No allocations - Uses enums and static dispatch (no
Box,dyn, or heap) - Embedded-ready -
#![no_std]compatible with zero dependencies by default - Stateful states - States can carry typed data
- Lifecycle hooks -
entry,process, andexitactions per state
Advanced (v0.3.0)
- Guards - Conditional transitions with boolean logic (security, validation, business rules)
- Logging - Optional instrumentation via
logortracingcrates (zero-cost when disabled) - Timeouts - Timer trait abstraction pattern for time-based transitions (platform-agnostic)
Concurrency & ISR Support (v0.4.0)
Enable the concurrent feature for interrupt-safe and thread-safe dispatch:
- ISR-Safe Dispatch - Safe to call from interrupt service routines in embedded systems
- Timer interrupts, UART interrupts, GPIO interrupts
- RTOS task + ISR combined scenarios
- Lock-free event queuing with FIFO ordering
- Multithreading - Safe concurrent access from multiple threads
- Atomic protection against re-entrant dispatch calls
- Automatic event queuing when dispatch is busy
- Configurable queue capacity per FSM (default: 16 events)
- Dropped Events Monitoring - Track and diagnose queue overflows
dropped_events_count()API for production monitoring- Debug mode panics on overflow for early detection
- Broad Architecture Support - Works on ARM Cortex-M, RISC-V, AVR, and Desktop (x86/ARM64) via
portable-atomic. - Performance - ~10-15% overhead when enabled, zero overhead when disabled
Why typed-fsm?
Comparison with Alternatives
| Feature | typed-fsm | statig | smlang | rust-fsm |
|---|---|---|---|---|
| Event-driven | ✓ | ✓ | ✓ | ✓ |
| Zero-cost | ✓ | ~ | ~ | ~ |
| no_std | ✓ | ✓ | ✓ | ✓ |
| Stateful states | ✓ | ✓ | ✓ | ✗¹ |
| Lifecycle hooks | ✓ | ✓ | ~² | ✗ |
| Hierarchical FSM | ~³ | ✓ | ✗ | ✗ |
| Thread-safe (Send+Sync) | ✓ | ? | ? | ? |
| ISR/Concurrency support | ✓⁴ | ✗⁵ | ✗⁵ | ✗⁵ |
| Macro-based DSL | ✓ | ✓ | ✓ | ✓ |
| Type-safe | ✓ | ✓ | ✓ | ✓ |
| Dependencies | 0⁶ | 3⁷ | 1 | 2⁷ |
| Async support | ✗⁸ | ✓ | ✓ | ✗ |
| Diagram generation | ✗ | ✗ | ✗ | ✓ |
¹ rust-fsm: States cannot carry data in DSL (manual implementation possible)
² smlang: Has guards/actions, but not explicit entry/exit hooks per state
³ typed-fsm: Via nested FSMs in context (compositional, not native like statig)
⁴ typed-fsm: Native support via concurrent feature. Atomic protection, event queuing, ISR-safe dispatch
⁵ No native ISR/concurrency support. Manual synchronization required (Arc<Mutex<>>, critical sections)
⁶ typed-fsm: Zero dependencies by default. Optional dependencies when features enabled: logging (+1 dep), concurrent (+3 deps)
⁷ Optional dependencies (can be disabled with feature flags)
⁸ typed-fsm: Can be used within async code, but hooks are synchronous (no async fn support)
When to Choose Each
Choose typed-fsm if you need:
- ISR-safe dispatch - Call
dispatch()from interrupt handlers (timer, UART, GPIO interrupts) - Thread-safe concurrency - Safe concurrent access with atomic protection and event queuing
- Absolute zero dependencies by default (embedded, security-critical)
- Guaranteed zero-cost abstraction with no runtime overhead
- Explicit lifecycle hooks (entry/process/exit)
- Clear thread-safety guarantees (auto Send+Sync)
- Simple API without hierarchical complexity
Choose statig if you need:
- Native hierarchical state machines with superstates
- Async/await support for concurrent state machines
- State-local storage (data bound to specific states)
- More mature ecosystem with extensive documentation
- Note: No native ISR support - requires manual synchronization
Choose smlang if you need:
- Async state machines out of the box
- Guards (conditional transitions) and actions built into DSL
- DSL-first approach with procedural macros
- Note: No native ISR support - requires manual synchronization
Choose rust-fsm if you need:
- Automatic Mermaid diagram generation for visualization
- Classical Mealy/Moore machine patterns
- Large community and battle-tested in production
- Flexible transition specifications
- Note: No native ISR support - requires manual synchronization
Don't choose typed-fsm if you need:
- Native async/await support (hooks are synchronous)
- Built-in hierarchical state machines (use composition instead)
- Automatic state diagram generation
Key Advantages of typed-fsm
- ISR-Safe and Thread-Safe Concurrency - The only Rust FSM library with native ISR-safe dispatch. Call
dispatch()from interrupt handlers (timer, UART, GPIO) or multiple threads with atomic protection and lock-free event queuing. Perfect for embedded systems and RTOS environments. - True Zero Dependencies by Default - No dependencies in default configuration, perfect for security-critical applications. Optional features add minimal, vetted dependencies only when needed (
logging: +1,concurrent: +3) - Genuine Zero-Cost Abstraction - Compiles to optimal code without procedural macro overhead. Concurrent feature adds only ~10-15% overhead when enabled, zero when disabled.
- Complete Lifecycle Model - Clean entry/process/exit pattern without DSL limitations
- Embedded-First - Designed for resource-constrained environments from day one with
#![no_std]support
Quick Start
Add this to your Cargo.toml:
[]
= "0.4"
Simplest Example: Blink
A minimal LED blink state machine:
use ;
// Context: Shared state
// Event: Simple tick
// State machine: On ↔ Off
state_machine!
🔄 Understanding Transitions
The process hook must return a Transition enum to tell the state machine what to do next:
Transition::None - Stay in Current State
Use when an event should be handled but doesn't require changing states:
process:
When to use:
- Event updates context but state remains the same
- Event should be ignored in this state
- Processing an event that doesn't affect state flow
What happens:
- ✅
processexecutes - ❌
exitdoes NOT execute (no state change) - ❌
entrydoes NOT execute (no state change) - ✅ State remains unchanged
Transition::To(State) - Move to New State
Use when an event should trigger a state change:
process:
When to use:
- Event triggers a state change
- Conditions are met for transitioning
- Need to move to a different state (including self-transitions)
What happens:
- ✅
processexecutes and returns new state - ✅ Current state's
exitexecutes (if defined) - ✅ New state's
entryexecutes (if defined) - ✅ State updates to the new state
Example: Combining Both
state_machine!
Key Points:
- Every
processblock must return aTransition - Use
Transition::Nonefor events that don't change state - Use
Transition::To(State)for events that trigger transitions - You can update context in
processbefore returning - The transition type determines whether
exit/entryhooks run
⚠️ Important: Initialization
You MUST call .init(&mut ctx) before dispatching any events!
Why is init() required?
The init() method executes the entry action of the initial state. Forgetting to call it will cause:
- The entry action of the initial state will NEVER execute
- The state machine will still process events, but initialization is skipped
- This can lead to incorrect behavior that is difficult to debug
Correct Usage Pattern
// 1. Create context
let mut ctx = MyContext ;
// 2. Create state machine
let mut fsm = InitialState;
// 3. ⚠️ CRITICAL: Initialize BEFORE event loop
fsm.init;
// 4. Now safe to dispatch events
loop
Common Mistake (Don't Do This!)
let mut ctx = MyContext ;
let mut fsm = InitialState;
// ❌ WRONG: Forgot to call init()!
// Entry action will NEVER execute!
fsm.dispatch; // Silent failure
See the blink example for a complete demonstration.
Quick Start Template
Copy and paste this template to start building your state machine. Replace the UPPERCASE placeholders with your actual names:
use ;
// 1. Define your context (shared state across all states)
// 2. Define your events (what can happen to trigger transitions)
// 3. Create your state machine
state_machine!
Advanced Features
Stateful States
States can carry typed data:
state_machine!
Lifecycle Hooks
Each state supports three lifecycle hooks:
entry(optional) - Executed once when entering the stateprocess(required) - Handles events, returnsTransition<S>exit(optional) - Executed once when leaving the state
state_machine!
Concurrency Support
typed-fsm supports concurrent state machines through composition with Rust's standard concurrency primitives.
Thread Safety
FSMs are automatically Send + Sync if their fields are Send + Sync. This allows safe sharing across threads using Arc<Mutex<>>.
use ;
use thread;
use ;
// FSM is automatically Send + Sync
let fsm = new;
let ctx = new;
// Clone for another thread
let fsm_clone = clone;
let ctx_clone = clone;
spawn;
Event Broadcasting
Use channels to distribute events to multiple FSMs:
use channel;
let = channel;
let rx2 = tx.clone;
// Broadcast events to multiple FSMs
tx.send.unwrap;
Coordinated FSMs
Multiple FSMs can coordinate through shared state:
let shared = new;
// FSM A and FSM B coordinate via shared state
// See examples/traffic_intersection.rs for complete example
Important: no_std Compatibility
The core framework remains #![no_std] compatible. Concurrency examples use std::sync and std::thread, but the generated FSM code has zero dependencies and works in no_std environments.
For embedded systems without std:
- Use
spin::Mutexinstead ofstd::sync::Mutex - Use
alloc::sync::Arcinstead ofstd::sync::Arc - Implement custom event distribution (e.g., interrupt-based)
Using with Async Code
While typed-fsm does not have native async/await support in lifecycle hooks, it can be used within async contexts. The state machine methods are synchronous but can be called from async functions.
Pattern: Async Wrapper
use ;
// Standard synchronous FSM
state_machine!
// Use within async context
async
Why No Native Async?
This is an intentional design decision to:
- Maintain zero-cost abstraction - Async has inherent overhead (futures, polling, state machines)
- Preserve no_std compatibility - Async requires a runtime (tokio, async-std)
- Keep API simple - Synchronous hooks are easier to reason about
- Support embedded systems - Many embedded environments don't use async
When You Need Async Hooks
If your use case requires async fn in entry/exit/process hooks, consider:
- statig - Native hierarchical async state machines
- smlang - Async actions and guards built-in
These crates sacrifice zero-cost abstraction and require a runtime, but provide first-class async support.
Guards (Conditional Transitions)
Guards are boolean conditions that must evaluate to true for a state transition to occur. They act as gatekeepers, validating data or checking preconditions before allowing state changes.
What are Guards?
Guards allow you to implement conditional logic that determines whether an event should trigger a state transition. This is essential for:
- Security checks - PIN verification, authentication
- Resource validation - Check availability before allocation
- Business rules - Enforce constraints and policies
- Data validation - Verify input before accepting
How Guards Work
Guards are implemented using standard Rust conditionals (if/else) within the process block. No special syntax is needed - just return Transition::None when the guard condition fails.
Example: ATM PIN Verification
use ;
state_machine!
Multiple Guard Conditions
Guards can combine multiple conditions:
process:
Guard Best Practices
- Early Returns - Return immediately when guard fails for clarity
- Logging - Log why guard failed for debugging
- Context Updates - Update context before returning (e.g., increment attempt counter)
- Clear Messages - Provide user feedback when guard blocks transition
Complete Example
See examples/guards.rs for a comprehensive example demonstrating:
- ATM security guards (PIN verification)
- Door lock access control
- Order processing business rules
Run with:
Timeouts (Time-Based Transitions)
typed-fsm supports time-based state transitions through the Timer trait abstraction pattern. This pattern maintains no_std compatibility while providing a flexible, platform-agnostic way to implement timeouts, retries, and time-based behaviors.
What are Timeouts?
Timeouts allow states to automatically transition after a specified time duration. They're essential for:
- Connection timeouts
- Retry mechanisms with exponential backoff
- Button debouncing
- Watchdog timers in embedded systems
- Session expiration
- Idle detection
Implementation Pattern
Unlike some FSM libraries that provide built-in timer functionality (which would break no_std compatibility), typed-fsm uses a trait abstraction pattern:
- Define a
Timertrait (user-provided or use the example) - Store timer instances in your Context
- Check timeouts in your process blocks
- Reset timers in entry/exit hooks as needed
This pattern is:
- Zero-cost - No overhead if you don't use it
- no_std compatible - Users implement for their platform
- Completely optional - Ignore if you don't need timeouts
- Platform-agnostic - Works with any time source
Timer Trait
Platform Implementations
For std (Desktop/Server):
use ;
For Embedded (no_std):
// Example for embedded HAL timer
For Testing (Mock):
// In tests, manually decrement remaining_ms to simulate time
Example: WiFi Connection with Timeout
use ;
state_machine!
Usage Pattern
let mut ctx = WiFiContext ;
let mut wifi = Connecting;
wifi.init;
// In your event loop:
loop
Best Practices
- Store timers in Context - Not in state variants (they get moved during transitions)
- Use polling pattern - Check timeouts via a
CheckTimeoutevent in your event loop - Reset on exit - Always reset timers in exit hooks to prevent stale timeouts
- Start on entry - Initialize timers when entering the state that needs them
- Platform abstraction - Implement Timer trait for your specific platform
Complete Example
See examples/timeouts.rs for comprehensive examples demonstrating:
- WiFi connection with timeout and retry logic
- Session timeout with idle detection
- Button debouncing with time delays
Run with:
Concurrency Support (Feature: concurrent)
NEW in v0.4.0: Built-in support for safe dispatch from interrupt service routines (ISRs) and multiple threads.
When to Use
Enable the concurrent feature when you need to call dispatch() from:
- Interrupt Service Routines (ISRs) - Timer interrupts, UART interrupts, GPIO interrupts
- Multiple Threads - Concurrent access from different threads
- RTOS Environments - Tasks + ISRs running simultaneously
How It Works
The concurrent feature adds atomic protection to prevent re-entrant dispatch calls:
- Immediate execution - If no dispatch is active, the event executes immediately
- Automatic queuing - If dispatch is busy, the event is queued (FIFO order)
- Queue processing - All queued events are processed before releasing the lock
- Zero overhead when disabled - Standard implementation has no concurrency cost
- Portable Atomics - Uses
portable-atomicfor broad architecture support (AVR, Cortex-M, RISC-V, Desktop).
Installation
[]
= { = "0.4", = ["concurrent"] }
# Requires critical-section implementation for your platform:
# - For std: critical-section with "std" feature (included automatically)
# - For embedded: Use your HAL's critical-section implementation
Dependencies added by concurrent feature:
critical-sectionv1.1 - Portable critical sections (interrupt-safe primitives)heaplessv0.8 - No-alloc data structures (event queue)pastev1.0 - Macro hygiene (static variable name generation)portable-atomicv1.0 - Portable atomic types for all architectures
Performance
- Without contention: ~10-15% overhead vs non-concurrent
- ISR enqueue: ~100 cycles (fast and deterministic)
- Without feature: Zero overhead (standard implementation)
Example: ISR Usage
// Embedded system with timer interrupt
static mut FSM: = None;
static mut CTX: = None;
Example: Multithreading
use ;
use thread;
let fsm = new;
let ctx = new;
// Thread 1: Producer
let = ;
spawn;
// Thread 2: Consumer
let = ;
spawn;
Complete Examples
# ISR simulation with event queuing
# Multithreading with concurrent dispatch
# Run concurrency tests
See:
examples/concurrent_isr.rs- Simulated ISR with atomic event queuingexamples/concurrent_threads.rs- Multi-threaded task processortests/concurrent_tests.rs- Comprehensive concurrency tests
Testing
This library has comprehensive test coverage (~100%) with 88+ tests covering:
- Unit tests (3 tests) - Core
Transitionenum functionality - Coverage tests (10 tests) - All lifecycle hooks, optional entry/exit, self-transitions, multi-field states
- Edge case tests (8 tests) - Early returns, nested patterns, wildcard matches, if-let patterns
- Integration tests (13 tests) - Complete FSM scenarios (toggle, counter, resources, concurrency)
- Guards tests (14 tests) - Conditional transitions (PIN verification, multiple guards, range checks)
- Logging tests (9 tests) - Zero-cost abstraction, lifecycle hooks, self-transitions
- Timeouts tests (11 tests) - Timer trait pattern, retry logic, session timeouts
- Concurrent tests (9 tests) - ISR safety, multithreading, FIFO ordering, high contention (v0.4.0)
- Doc tests (11+ tests) - All documentation examples are tested
Run all tests:
# Run all tests (without concurrent feature)
# Run tests with concurrent feature
# Run tests with output
# Run specific test suites
Examples
See the examples/ directory for complete examples:
- **[New]** Raspberry Pi Pico 2 W Demo: [typed-fsm-pico-test](https://github.com/afmiguel/typed-fsm-pico-test) - Real-world usage on RP2350 interacting with Hardware (LED, ADC, Timer).
- **[New]** Arduino Uno Demo: [typed-fsm-arduino-test](https://github.com/afmiguel/typed-fsm-arduino-test) - Real-world usage on ATmega328P (AVR) with concurrent ISRs.
motor.rs: Motor control system (Complex, Event-Driven) - Start here!traffic_light.rs: Traffic light controller (Simple, Event-Driven)guards.rs: Conditional transitions (ATM, Door Lock, Shop Orders)logging.rs: FSM with logging instrumentationtimeouts.rs: Timer patterns (WiFi Connection, Session Timeout, Debouncing)concurrent_isr.rs: ISR-safe dispatch (requiresconcurrentfeature)concurrent_threads.rs: Thread-safe dispatch (requiresconcurrentfeature)
How It Works
The state_machine! macro generates:
- A
pub enumwith your states as variants - Implementation of
init(),dispatch(), and internal lifecycle methods - Type-safe state transitions with compile-time validation
The generated code uses:
- Static dispatch (no
dyn Trait) - Move semantics for state transitions
#[inline(always)]for zero-cost abstraction
Performance
This library is designed for performance-critical applications:
- Zero heap allocations - All state data is stack-allocated
- Optimal codegen - Compiles to efficient jump tables
- No runtime overhead - State transitions are simple enum assignments
- Embedded-friendly - No dependencies,
#![no_std]compatible
Use Cases
- Embedded systems and firmware
- Protocol implementations
- Game logic
- UI state management
- Robotics and control systems
- Workflow engines
FAQ
General Questions
Q: Can I use typed-fsm in no_std environments?
A: Yes! typed-fsm is #![no_std] compatible and has zero dependencies, making it perfect for embedded systems.
Q: What's the performance overhead? A: Zero! The macro compiles to simple enum pattern matching. State transitions are just enum assignments. No heap allocations, no dynamic dispatch.
Q: Can states hold data?
A: Yes! States can carry typed fields, for example: Running { speed: u32, mode: Mode }.
Q: How does this compare to manual enum matching? A: It generates the same code you would write manually, but with better organization, lifecycle hooks, and less boilerplate.
Technical Questions
Q: Does this work with async/await? A: Yes! The context and events can be async-friendly. The state machine itself is synchronous, but you can use async operations in your entry/exit/process handlers.
Q: Can I serialize the state machine?
A: The generated enum can derive Serialize/Deserialize if you enable the serde feature (future enhancement).
Q: How do I handle errors in state transitions?
A: You can include error information in events or state data. For example: Error { code: u32, message: String }.
Q: Can I have nested state machines? A: Yes! A state's context can contain another state machine. This allows hierarchical state machines.
Safety Questions
Q: Is this library safe?
A: Yes! The library contains zero unsafe blocks and has zero dependencies by default. It's been thoroughly tested with 100+ tests covering 100% of code paths.
Q: Can invalid states occur? A: No! Rust's type system prevents invalid states at compile time. If it compiles, the state transitions are valid.
Q: Is this production-ready? A: Yes! The library has comprehensive tests, documentation, and follows Rust best practices. It's ready for production use.
Guards & Conditional Transitions (v0.3.0)
Q: How do I implement conditional transitions (guards)?
A: Use if conditions inside your process handler:
process:
Q: Can I have complex guard conditions?
A: Yes! Use any Rust expression. Guards can check context state, event data, external conditions, etc. For multiple conditions, use if/else if/else chains or helper functions.
Q: Do I need a special feature for guards?
A: No! Guards are just normal Rust code in your process handler. No special syntax or features required.
Logging & Observability (v0.3.0)
Q: How do I add logging to my FSM?
A: Enable the logging feature:
[]
= { = "0.4", = ["logging"] }
This automatically logs all state transitions, entry/exit actions, and events using the log crate.
Q: Does logging add overhead when disabled?
A: Zero overhead! When the logging feature is disabled, no logging code is generated at all. It's a true zero-cost abstraction.
Q: Can I use tracing instead of log?
A: Not yet, but it's planned. Currently only the log crate is supported via the logging feature.
Timeouts & Time-Based Transitions (v0.3.0)
Q: How do I implement timeouts?
A: Implement a Timer trait and check elapsed time in your process handler:
process:
See examples/timeouts.rs for complete implementations.
Q: Do I need a specific timer library?
A: No! The timer pattern is platform-agnostic. Use std::time::Instant for std environments, or your HAL's timer for embedded systems.
ISR & Interrupt Safety (v0.4.0)
Q: When do I need the concurrent feature?
A: Enable concurrent when you need to call dispatch() from:
- Interrupt Service Routines (ISRs): Timer interrupts, UART interrupts, GPIO interrupts
- Multiple threads: Concurrent access from different threads
- RTOS environments: Tasks + ISRs running simultaneously
Q: What's the difference between Arc<Mutex> and the concurrent feature? A:
- Arc<Mutex>: Thread-safe but NOT ISR-safe. Cannot be used in interrupt handlers.
- concurrent feature: Both thread-safe AND ISR-safe. Uses atomic operations and lock-free queuing specifically designed for interrupt contexts.
Q: How does ISR-safe dispatch work? A: When dispatch is called from an ISR while another dispatch is active:
- The event is atomically queued (FIFO order)
- No blocking or waiting occurs
- The active dispatch processes all queued events before returning
- Fast and deterministic (~100 cycles for ISR enqueue)
Q: Can I use dispatch() directly in an interrupt handler?
A: Yes, with the concurrent feature enabled:
Concurrency & Thread Safety (v0.4.0)
Q: What's the performance overhead of the concurrent feature? A: ~10-15% overhead when enabled with no contention. Zero overhead when the feature is disabled (default).
Q: Do I need critical-section for the concurrent feature?
A: Yes. For std environments, it's automatically included. For embedded/no_std, you need to provide a critical-section implementation from your HAL.
Q: Can events be dropped?
A: Yes, if the queue is full (default: 16 events). Use dropped_events_count() to monitor:
if dropped_events_count > 0
Q: How do I configure queue size?
A: Use the QueueCapacity parameter:
state_machine!
Q: What happens in debug vs release builds when queue overflows? A:
- Debug builds: Panic immediately to catch issues during development
- Release builds: Silent drop with atomic counter increment for production monitoring
Q: My events need to be Clone for concurrent feature. Why? A: Events are cloned when queued. This allows the ISR/thread to return immediately without waiting. Most event types are small and cheap to clone.
Documentation
Full API documentation is available at docs.rs/typed-fsm.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.