6个版本

0.3.0 2023年3月25日
0.2.0 2023年1月5日
0.2.0-beta.02022年12月7日
0.1.0 2022年11月9日

#6 in #状态图

Download history 112/week @ 2024-03-25 76/week @ 2024-04-01 119/week @ 2024-04-08 139/week @ 2024-04-15 138/week @ 2024-04-22 14/week @ 2024-04-29 34/week @ 2024-05-06 24/week @ 2024-05-13 44/week @ 2024-05-20 64/week @ 2024-05-27 63/week @ 2024-06-03 170/week @ 2024-06-10 80/week @ 2024-06-17 115/week @ 2024-06-24 82/week @ 2024-07-01 126/week @ 2024-07-08

440 每月下载量
用于 statig

MIT 许可证

105KB
2K SLoC

statig

Current crates.io version Documentation Rust version CI

用于设计事件驱动系统的分层状态机。

特性

  • 分层状态机
  • 状态本地存储
  • 兼容 #![no_std],状态机在只读存储器(ROM)中定义,没有堆内存分配。
  • (可选) 用于减少模板代码的宏。
  • 支持泛型。
  • 支持异步动作和处理器(仅在 std 中)。

概述


Statig 在行动

一个简单的闪烁状态机

┌─────────────────────────┐                   
│         Blinking        │◀─────────┐        
│    ┌───────────────┐    │          │        
│ ┌─▶│     LedOn     │──┐ │  ┌───────────────┐
│ │  └───────────────┘  │ │  │  NotBlinking  │
│ │  ┌───────────────┐  │ │  └───────────────┘
│ └──│     LedOff    │◀─┘ │          ▲        
│    └───────────────┘    │──────────┘        
└─────────────────────────┘                   
#[derive(Default)]
pub struct Blinky;

pub enum Event {
    TimerElapsed,
    ButtonPressed
}

#[state_machine(initial = "State::led_on()")]
impl Blinky {
    #[state(superstate = "blinking")]
    fn led_on(event: &Event) -> Response<State> {
        match event {
            Event::TimerElapsed => Transition(State::led_off()),
            _ => Super
        }
    }

    #[state(superstate = "blinking")]
    fn led_off(event: &Event) -> Response<State> {
        match event {
            Event::TimerElapsed => Transition(State::led_on()),
            _ => Super
        }
    }

    #[superstate]
    fn blinking(event: &Event) -> Response<State> {
        match event {
            Event::ButtonPressed => Transition(State::not_blinking()),
            _ => Super
        }
    }

    #[state]
    fn not_blinking(event: &Event) -> Response<State> {
        match event {
            Event::ButtonPressed => Transition(State::led_on()),
            _ => Super
        }
    }
}

fn main() {
    let mut state_machine = Blinky::default().state_machine();

    state_machine.handle(&Event::TimerElapsed);
    state_machine.handle(&Event::ButtonPressed);
}

(查看带有注释的完整代码的macro/blinky 示例。或者查看不使用宏的版本 no_macro/blinky).


概念

状态

通过在 impl 块内部编写方法并将 #[state] 属性添加到它们中来定义状态。当向状态机提交事件时,将调用与当前状态关联的方法来处理它。默认情况下,此事件映射到方法的 event 参数。

#[state]
fn led_on(event: &Event) -> Response<State> {
    Transition(State::led_off())
}

每个状态都必须返回一个 Response。一个 Response 可以是以下三者之一

  • Handled: 事件已处理。
  • Transition:转换到另一个状态。
  • Super:将事件推迟到父超级状态。

超级状态

超级状态允许您创建状态层次结构。状态可以通过返回 Super 响应将事件推迟到它们的超级状态。

#[state(superstate = "blinking")]
fn led_on(event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => Transition(State::led_off()),
        Event::ButtonPressed => Super
    }
}

#[superstate]
fn blinking(event: &Event) -> Response<State> {
    match event {
        Event::ButtonPressed => Transition(State::not_blinking()),
        _ => Super
    }
}

超级状态本身也可以有超级状态。

动作

动作在转换过程中进入或离开状态时运行。

#[state(entry_action = "enter_led_on", exit_action = "exit_led_on")]
fn led_on(event: &Event) -> Response<State> {
    Transition(State::led_off())
}

#[action]
fn enter_led_on() {
    println!("Entered on");
}

#[action]
fn exit_led_on() {
    println!("Exited on");
}

共享存储

如果您的状态机实现类型有任何字段,您可以在所有状态、超级状态或动作中访问它们。

#[state]
fn led_on(&mut self, event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => {
            self.led = false;
            Transition(State::led_off())
        }
        _ => Super
    }
}

或者,在进入动作中设置 led

#[action]
fn enter_led_off(&mut self) {
    self.led = false;
}

状态本地存储

有时您有只存在于某种状态中的数据。您可以将此数据添加到共享存储中,并可能需要解包一个 Option<T>,或者将数据作为输入添加到您的状态处理器。

#[state]
fn led_on(counter: &mut u32, event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => {
            *counter -= 1;
            if *counter == 0 {
                Transition(State::led_off())
            } else {
                Handled
            }
        }
        Event::ButtonPressed => Transition(State::led_on(10))
    }
}

counter 只在 led_on 状态中可用,但也可以在它的超级状态和动作中访问。

上下文

当状态机用于更大的系统时,有时需要传递外部可变上下文。默认情况下,此上下文映射到方法的 context 参数。

#[state]
fn led_on(context: &mut Context, event: &Event) -> Response<State> {
    match event {
        Event::TimerElapsed => {
            context.do_something();
            Handled
        }
        _ => Super
    }
}

然后您必须使用 handle_with_context 方法提交事件到状态机。

state_machine.handle_with_context(&Event::TimerElapsed, &mut context);

内省

为了日志记录目的,您可以在状态机执行过程中特定点调用两个回调。

  • on_dispatch 在将事件分发到特定状态或超级状态之前调用。
  • on_transition 在转换发生后调用。
#[state_machine(
    initial = "State::on()",
    on_dispatch = "Self::on_dispatch",
    on_transition = "Self::on_transition",
    state(derive(Debug)),
    superstate(derive(Debug))
)]
impl Blinky {
    ...
}

impl Blinky {
    fn on_transition(&mut self, source: &State, target: &State) {
        println!("transitioned from `{:?}` to `{:?}`", source, target);
    }

    fn on_dispatch(&mut self, state: StateOrSuperstate<Blinky>, event: &Event) {
        println!("dispatched `{:?}` to `{:?}`", event, state);
    }
}

异步

所有处理器和动作都可以设置为异步。目前这仅在 std 中可用,并需要启用 async 功能)。

#[state_machine(initial = "State::led_on()")]
impl Blinky {
    #[state]
    async fn led_on(event: &Event) -> Response<State> {
        match event {
            Event::TimerElapsed => Transition(State::led_off()),
            _ => Super
        }
    }
}

然后 #[state_machine] 宏将自动检测正在使用异步函数,并生成异步状态机。

async fn main() {
    let mut state_machine = Blinky::default().state_machine();

    state_machine.handle(&Event::TimerElapsed).await;
    state_machine.handle(&Event::ButtonPressed).await;
}

实现

许多实现细节由 #[state_machine] 宏处理,但了解幕后发生的事情始终很有价值。此外,您会看到生成的代码实际上是相当直接的,可以很容易地手动编写,因此如果您更喜欢避免使用宏,这也是完全可行的。

statig 的目标是表示分层状态机。从概念上讲,分层状态机可以被认为是树。

                          ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐             
                                    Top                       
                          └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘             
                                     │                        
                        ┌────────────┴────────────┐           
                        │                         │           
             ┌─────────────────────┐   ╔═════════════════════╗
             │      Blinking       │   ║     NotBlinking     ║
             │─────────────────────│   ╚═════════════════════╝
             │ counter: &'a usize  │                          
             └─────────────────────┘                          
                        │                                     
           ┌────────────┴────────────┐                        
           │                         │                        
╔═════════════════════╗   ╔═════════════════════╗             
║        LedOn        ║   ║        LedOff       ║             
║─────────────────────║   ║─────────────────────║             
║ counter: usize      ║   ║ counter: usize      ║             
╚═════════════════════╝   ╚═════════════════════╝

树的边缘节点称为叶状态,在 statig 中由枚举表示。如果数据只存在于某个特定状态,我们可以将数据的所有权赋予该状态。这被称为“状态本地存储”。例如,counter 只存在于 LedOnLedOff 状态。

enum State {
    LedOn { counter: usize },
    LedOff { counter: usize },
    NotBlinking
}

Blinking 之类的状态称为超级状态。它们定义了其子状态共享的行为。超级状态也由枚举表示,但它们不是拥有自己的数据,而是从底层状态借用数据。

enum Superstate<'sub> {
    Blinking { counter: &'sub usize }
}

状态与其处理器的关联通过StateSuperstate特性中的call_handler方法来表示。

impl statig::State<Blinky> for State {
    fn call_handler(&mut self, blinky: &mut Blinky, event: &Event) -> Response<Self> {
        match self {
            State::LedOn { counter } => blinky.led_on(counter, event),
            State::LedOff { counter } => blinky.led_off(counter, event),
            State::NotBlinking => blinky.not_blinking(event)
        }
    }
}

impl statig::Superstate<Blinky> for Superstate {
    fn call_handler(&mut self, blinky: &mut Blinky, event: &Event) -> Response<Self> {
        match self {
            Superstate::Blinking { counter } => blinky.blinking(counter, event),
        }
    }
}

状态与其动作的关联以类似的方式表示。

impl statig::State<Blinky> for State {
    
    ...

    fn call_entry_action(&mut self, blinky: &mut Blinky) {
        match self {
            State::LedOn { counter } => blinky.enter_led_on(counter),
            State::LedOff { counter } => blinky.enter_led_off(counter),
            State::NotBlinking => blinky.enter_not_blinking()
        }
    }

    fn call_exit_action(&mut self, blinky: &mut Blinky) {
        match self {
            State::LedOn { counter } => blinky.exit_led_on(counter),
            State::LedOff { counter } => blinky.exit_led_off(counter),
            State::NotBlinking => blinky.exit_not_blinking()
        }
    }
}

impl statig::Superstate<Blinky> for Superstate {

    ...

    fn call_entry_action(&mut self, blinky: &mut Blinky) {
        match self {
            Superstate::Blinking { counter } => blinky.enter_blinking(counter),
        }
    }

    fn call_exit_action(&mut self, blinky: &mut Blinky) {
        match self {
            Superstate::Blinking { counter } => blinky.exit_blinking(counter),
        }
    }
}

状态及其超级状态的树形结构通过StateSuperstate特性中的superstate方法来表示。

impl statig::State<Blinky> for State {

    ...

    fn superstate(&mut self) -> Option<Superstate<'_>> {
        match self {
            State::LedOn { counter } => Some(Superstate::Blinking { counter }),
            State::LedOff { counter } => Some(Superstate::Blinking { counter }),
            State::NotBlinking => None
        }
    }
}

impl<'sub> statig::Superstate<Blinky> for Superstate<'sub> {

    ...

    fn superstate(&mut self) -> Option<Superstate<'_>> {
        match self {
            Superstate::Blinking { .. } => None
        }
    }
}

当事件到达时,statig首先将其调度到当前叶状态。如果此状态返回一个Super响应,它将被调度到该状态的超级状态,然后该超级状态返回其自身的响应。每次将事件推迟到超级状态时,statig都会在图中向上遍历,直到达到Top状态。这是一个隐式超级状态,将考虑每个事件已被处理。

如果返回的响应是Transitionstatig将通过遍历图从当前源状态到目标状态(通过取最短路径)来执行转换序列。当路径从源状态向上时,每个经过的状态都会执行其退出动作。然后类似地,当向下移动时,每个经过的状态都会执行其进入动作

例如,从LedOn状态转换到NotBlinking状态的转换序列如下所示

  1. 退出LedOn状态
  2. 退出Blinking状态
  3. 进入NotBlinking状态

相比之下,从LedOn状态到LedOff状态的转换如下所示

  1. 退出LedOn状态
  2. 进入LedOff状态

我们不会执行Blinking的退出或进入动作,因为此超级状态在LedOnLedOff状态之间是共享的。

进入和退出动作也可以访问状态局部存储,但请注意,退出动作作用于源状态的状态局部存储,而进入动作作用于目标状态的状态局部存储。

例如,在LedOn的退出动作中改变counter的值,将不会对LedOff状态中的counter的值产生影响。

最后,StateMachine特性实现在用于共享存储的类型上。

impl IntoStateMachine for Blinky {
    type State = State;

    type Superstate<'sub> = Superstate<'sub>;

    type Event<'evt> = Event;

    type Context<'ctx> = Context;

    const INITIAL: State = State::off(10);
}

常见问题解答

这个#[state_machine]宏对我的代码做了什么?😮

简短回答:没有。#[state_machine]只是解析底层的impl块,并根据其内容派生一些代码并将其添加到你的源文件中。你的代码仍然会存在,没有任何改变。事实上,#[state_machine]可以是一个派生宏,但截至目前,Rust只允许派生宏在枚举和结构体上使用。如果你想看看生成的代码是什么样的,请查看带有宏和没有宏的测试 没有宏的测试

这种模式相比使用类型状态模式有哪些优势呢?

我认为它们服务于不同的目的。类型状态模式(参考链接)在设计API时非常有用,因为它可以通过将每个状态作为一个独特的类型来强制在编译时执行操作的有效性。但是,statig 是为了模拟一个动态系统而设计的,其中事件从外部起源,操作的顺序是在运行时确定的。更具体地说,这意味着状态机将处于一个循环中,其中事件从队列中读取,并使用 handle() 方法提交给状态机。如果我们想使用使用类型状态模式的状态机做同样的事情,我们就必须使用枚举来封装我们所有的不同状态,并将事件匹配到这些状态的操作上。这意味着会有额外的样板代码,但优势很小,因为操作的顺序是未知的,所以它不能在编译时进行检查。另一方面,statig 允许您创建状态的层次结构,我认为这对于状态机复杂性增长来说是无价的。


致谢

这个库的想法来自阅读书籍 《Practical UML Statecharts in C/C++》。如果您想学习如何使用状态机来设计复杂系统,我强烈推荐这本书。

依赖项

~1.5MB
~36K SLoC