Skip to content
robolibsPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

stateup

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.

Status

  • 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 datapod from GitHub)

Install

[dependencies]
stateup = { path = "../stateup_rs" }

Feature flags

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)

Quickstart: Rust

Behavior tree

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_selector
  • inverter, succeeder, failer, repeat, retry, retry_with_backoff, timeout, cooldown, cooldown_with_jitter
  • while_loop, for_loop, memory, debounce
  • Per-child metadata: with_score, with_weight, with_probability, with_priority

State machine

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

Event-driven run-to-completion

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` feature

Mermaid + DOT export

use 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`

C ABI

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,serde

Then 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

Python bindings

Build and install the Python wheel with maturin:

cd examples/python_binding && make develop
# or directly:
cargo check --features python
maturin develop --features python-extension
import 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()).

Examples

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

C demos

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

Python demos

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

Architecture

 ┌────────────────────────────────────────────────────────────┐
 │                       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 a parking_lot::RwLock with 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 during tick().
  • The Python bindings are implemented in src/python.rs using pyo3 = 0.23 and exposed via maturin. Same depth as the C ABI, with Python-native ergonomics (dynamic set/get, callable observers, string status enum).

Building and testing

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_binding

License

MIT.

Credits

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages