Behavior trees and hierarchical state machines for Rust, with deep C ABI and Python bindings.
stateup is a Rust port of the C++ stateup library with additional
features drawn from the wider Rust ecosystem. It ships two complementary
decision-making subsystems unified by a shared, type-safe blackboard:
- Behavior trees — composite, decorator, leaf, control-flow, utility, and advanced nodes for tick-driven AI, robotics, and orchestration
- Hierarchical state machines — guarded transitions, timed and probabilistic transitions, nested composite states with orthogonal regions and history, typed events with run-to-completion semantics, event bubbling, deferred events, and a typed enum-based parallel API
Both subsystems are driven from Rust, from C (via a callback-based C ABI),
and from Python (via PyO3) with the same expressiveness. The crate is
deliberately dependency-light: the default build pulls in only
rayon, parking_lot, thiserror, and rand.
- Feature-complete behavior tree and state machine implementations
- Depth parity with the C++ source plus several ecosystem-inspired additions (event-driven FSM, typed state enum API, tree loader, snapshot serialization, Mermaid exporter, tracing integration, async actions + async FSM hooks behind a feature flag)
- Git-dependency based for now, not configured for crates.io
- Local-only (depends on
datapodfrom GitHub)
[dependencies]
stateup = { path = "../stateup_rs" }| flag | adds |
|---|---|
| (default) | tree + FSM + blackboard + DOT/Mermaid export |
loader |
JSON tree loader with a built-in leaf-action registry |
serde |
state machine snapshot save/restore + blackboard serializer |
async |
AsyncAction tree leaves and on_enter_async / on_guard_async FSM hooks (std-only, no runtime) |
tracing |
tracing spans on tree ticks, state transitions, and blackboard writes |
python |
PyO3 bindings (implies loader) |
python-extension |
Python wheel build target (used by maturin) |
use stateup::{Status, TreeBuilder};
let mut tree = TreeBuilder::new()
.sequence()
.named_action("greet", |bb| {
bb.set("name", "Alice".to_string());
Status::Success
})
.selector()
.named_action("try_a", |_| Status::Failure)
.named_action("try_b", |_| Status::Success)
.end()
.end()
.build();
assert_eq!(tree.tick(), Status::Success);Composites, decorators, and control-flow nodes supported in the builder:
sequence,selector,parallel,random_selector,probability_selector,weighted_random_selector,utility_selector,one_shot_sequence,reactive_sequence,conditional_sequence,dynamic_selectorinverter,succeeder,failer,repeat,retry,retry_with_backoff,timeout,cooldown,cooldown_with_jitterwhile_loop,for_loop,memory,debounce- Per-child metadata:
with_score,with_weight,with_probability,with_priority
use stateup::StateBuilder;
#[derive(Clone, Debug)]
struct ButtonPressed;
let mut machine = StateBuilder::new()
.initial("idle")
.state("idle")
.on_enter(|_| println!("→ idle"))
.on_event::<ButtonPressed, _>("active", |_, _| true)
.state("active")
.on_enter(|_| println!("→ active"))
.build();
machine.tick();
machine.handle(ButtonPressed);
assert_eq!(machine.current_state_name(), Some("active"));The state builder supports:
- Per-state hooks:
on_enter,on_update,on_exit,on_guard - Transition kinds: predicate guards, timed (
transition_to_after), typed events (on_event::<E, _>,on_event_from_any), wildcard-from transitions, internal transitions (no enter/exit) - Transition modifiers:
with_action,with_priority,with_probability,with_weight,ignore_event,cannot_happen - Composite states with
HistoryType::{None, Shallow, Deep}and orthogonal regions - Event deferral:
defer_event::<E>()queues an event type while the state is active and replays it on exit - Typed state enum API via
trait StateKey+TypedStateBuilder<S> - Executor-backed parallel guard evaluation via rayon
machine.handle(MyEvent); // dispatches and drains any queued events
machine.go_to_previous(); // transition back using state history
let json = stateup::state::snapshot::StateMachineSnapshot::capture(&machine)
.to_json()?; // `serde` featureuse stateup::visualization::{
tree_to_dot, tree_to_mermaid,
state_machine_to_dot, state_machine_to_mermaid,
};
println!("{}", tree_to_mermaid(&tree)); // copies into GitHub MD
println!("{}", state_machine_to_dot(&machine)); // feed to `dot -Tsvg`The C ABI mirrors the full runtime surface — not just a "simple subset".
Callbacks cross the boundary as void* userdata plus typed function
pointers, and the shared-mode StateupBlackboard handed to each
callback resolves through a thread-local set by tick().
The public header lives at include/stateup.h.
Build libstateup.so (cdylib) with:
cargo build --features loader,serdeThen link: cc -I./include mycode.c -L./target/debug -lstateup.
#include "stateup.h"
static StateupStatus act_greet(StateupBlackboard* bb, void* ud) {
(void)ud;
stateup_blackboard_set_string(bb, "name", "Alice");
return STATEUP_STATUS_SUCCESS;
}
int main(void) {
StateupTreeBuilder* b = stateup_tree_builder_new();
stateup_tree_builder_sequence(b);
stateup_tree_builder_named_action(b, "greet", act_greet, NULL);
stateup_tree_builder_end(b);
StateupTree* tree = stateup_tree_builder_build(b);
stateup_tree_tick(tree);
char* name = stateup_tree_bb_get_string(tree, "name");
printf("hello, %s\n", name);
stateup_string_free(name);
stateup_tree_free(tree);
}What the C ABI exposes:
- Blackboard — typed set/get for int/double/bool/string,
has,remove,clear,keys, scope push, observer callback - Tree builder — every composite/decorator/control-flow node type with callback-based actions, conditions, and per-child metadata
- State machine builder — callback-driven
on_enter/on_update/on_exit/on_guard, predicate and event-driven transitions, timed transitions, priority/probability/weight, transition actions, internal / ignore_event / cannot_happen markers, wildcard events - Runtime — tick, reset, halt,
handle_event,go_to_previous, state history queries, DOT / Mermaid export, snapshot save/restore
Build and install the Python wheel with maturin:
cd examples/python_binding && make develop
# or directly:
cargo check --features python
maturin develop --features python-extensionimport stateup
# Tree
b = stateup.TreeBuilder()
b.sequence()
b.named_action("greet", lambda bb: (bb.set("name", "Alice"), "success")[1])
b.selector()
b.named_action("try_a", lambda bb: "failure")
b.named_action("try_b", lambda bb: "success")
b.end()
b.end()
tree = b.build()
print(tree.tick()) # 'success'
print(tree.get("name")) # 'Alice'
# State machine
sm = (
stateup.StateMachineBuilder()
.initial("red")
.state("red").transition_to("green", lambda bb: True)
.state("green").transition_to("yellow", lambda bb: True)
.state("yellow").transition_to("red", lambda bb: True)
.build()
)
for _ in range(5):
sm.tick()
print(sm.current_state())Python exposes the same deep surface as the C ABI — Blackboard,
TreeBuilder, Tree, StateMachineBuilder, StateMachine — plus
Python-friendly idioms (dynamic set/get, observer callables,
to_mermaid(), snapshot_json(), restore_json()).
Runnable Rust examples live in examples/. Representative
ones:
| example | what it shows |
|---|---|
getting_started.rs |
sequence + action + blackboard |
state_machine_demo.rs |
traffic-light FSM with enter/update/exit |
hierarchical_states_demo.rs |
nested composite states with history |
event_driven.rs |
typed handle(event) + event bubbling |
parallel_executor.rs |
rayon-backed Parallel node |
typed_state_machine.rs |
TypedStateBuilder<S> with an enum |
json_tree.rs |
loader feature: build a tree from JSON |
Six rich C programs live in
examples/c_abi/ along with a Makefile. From that
directory, make run builds libstateup.so and runs all six:
| demo | what it shows |
|---|---|
01_blackboard_tour.c |
every blackboard op + observer callbacks |
02_tree_guard_dog.c |
selector / sequence + callback actions + DOT |
03_tree_patrol_with_decorators.c |
repeat / while_loop / retry_with_backoff / inverter |
04_state_machine_hooks.c |
on_enter/update/exit/guard + transition actions + emergency event + Mermaid |
05_state_machine_mission.c |
rover mission FSM with priorities, events, telemetry |
06_snapshot_persistence.c |
serialize to JSON, restore in a fresh machine |
Six matching Python demos live in
examples/python_binding/. make all drives
maturin develop in a venv and runs them:
| demo | what it shows |
|---|---|
01_blackboard_tour.py |
typed + dynamic set/get + observer |
02_tree_roomba.py |
utility_selector / sequence / retry / callback actions |
03_tree_decorators_showcase.py |
every decorator + control-flow node |
04_state_machine_vending.py |
event-driven FSM with transition actions |
05_state_machine_npc.py |
game-AI FSM with priorities and wildcard events |
06_snapshot_and_mermaid.py |
snapshot JSON round-trip + Mermaid export |
┌────────────────────────────────────────────────────────────┐
│ stateup::core │
│ Error ┆ Executor (rayon) ┆ RecursionGuard │
└─────────────┬──────────────────────────────┬───────────────┘
│ │
┌─────────────▼───────────┐ ┌───────────▼──────────────┐
│ stateup::tree │ │ stateup::state │
│ │ │ │
│ Status / Node / Tree │ │ State / Transition │
│ TreeBuilder │ │ StateMachine │
│ Blackboard ◄──────────┼──────┼────► StateBuilder │
│ Composites/Decorators │ │ CompositeState │
│ Control-flow / Util │ │ TypedStateBuilder<S> │
│ (optional) Loader, │ │ (optional) Snapshot │
│ testing helpers │ │ Async hooks │
└─────────────┬───────────┘ └───────────┬──────────────┘
│ │
└──────────────┬───────────────┘
│
┌───────────▼────────────┐
│ stateup::ffi │ ← libstateup.so
│ stateup::python │ ← maturin wheel
│ visualization (DOT, │
│ Mermaid) │
└────────────────────────┘
- The blackboard is a type-erased
HashMap<String, Box<dyn Any>>under aparking_lot::RwLockwith RAII scopes and an observer hook. Both trees and state machines share the same type. - The C ABI is a single file (
src/ffi.rs) with opaque handles (StateupBlackboard,StateupTree,StateupStateMachine, builders) and thread-local routing so callbacks receive a stable shared-mode blackboard pointer duringtick(). - The Python bindings are implemented in
src/python.rsusingpyo3 = 0.23and exposed viamaturin. Same depth as the C ABI, with Python-native ergonomics (dynamicset/get, callable observers, string status enum).
cargo build # core crate only
cargo build --features loader,serde # cdylib for C use
cargo test --features async,loader,serde,tracing # 74 tests
# C demos
make -C examples/c_abi run
# Python demos (needs maturin + python3-venv)
make -C examples/python_bindingMIT.
This is a Rust re-imagining of the C++ stateup library by the same
author. The hierarchical state-machine and behavior-tree semantics
follow that reference; the ecosystem additions (event bubbling, typed
state enums, Mermaid export, tree loader, async hooks, snapshot, etc.)
are drawn from statig, behavior_tree_lite, and bonsai-bt patterns
in the Rust ecosystem.