18个版本

0.8.0 2024年8月9日
0.7.0 2024年7月3日
0.6.0 2022年11月2日
0.5.1 2022年2月5日
0.1.1 2019年5月6日

#978嵌入式开发

Download history 2192/week @ 2024-04-29 2474/week @ 2024-05-06 2431/week @ 2024-05-13 2499/week @ 2024-05-20 2592/week @ 2024-05-27 1761/week @ 2024-06-03 2270/week @ 2024-06-10 2675/week @ 2024-06-17 2263/week @ 2024-06-24 2286/week @ 2024-07-01 3066/week @ 2024-07-08 2754/week @ 2024-07-15 2150/week @ 2024-07-22 2475/week @ 2024-07-29 2571/week @ 2024-08-05 2606/week @ 2024-08-12

9,867 每月下载量
9 个crate中使用(通过 smlang

MIT/Apache

94KB
2K SLoC

smlang: Rust中的no_std状态机语言领域特定语言

Build Status Documentation

基于Boost-SML语法的状态机语言领域特定语言。

smlang是一个过程宏库,创建了一个状态机语言领域特定语言,其目的是为了简化状态机的使用,因为它们很快就会变得过于复杂,难以编写和概述。

该库支持异步和非异步代码。

转换领域特定语言

以下是一个领域特定语言的示例。有关statemachine宏的完整描述,请参阅领域特定语言文档

statemachine!{
    transitions: {
        *SrcState1 + Event1 [ guard1 ] / action1 = DstState2, // * denotes starting state
        SrcState2 + Event2 [ guard2 ] / action2 = DstState1,
    }
    // ...
}

其中guardaction是可选的,可以省略。一个guard是一个函数,如果状态转换应该发生,则返回Ok(true) - 否则,转换不应发生。在状态机转换期间运行action函数。

这意味着任何状态机都必须以转换列表的形式编写。

领域特定语言支持通配符和与Rust模式匹配类似的输入状态模式匹配

statemachine!{
    transitions: {
        *State1 | State3 + ToState2 = State2,
        State1 | State2 + ToState3 = State3,
        _ + ToState4 = State4,
        State4 + ToState1 = State1,
    }
    // ...
}

这等价于

statemachine!{
    transitions: {
        *State1 + ToState2 = State2,
        State3 + ToState2 = State2,

        State1 + ToState3 = State3,
        State2 + ToState3 = State3,

        State1 + ToState4 = State4,
        State2 + ToState4 = State4,
        State3 + ToState4 = State4,
        State4 + ToState4 = State4,

        State4 + ToState1 = State1,
    }
    // ...
}

请参阅examples/input_state_pattern_match.rs中的使用示例。

内部转换

领域特定语言支持内部转换。内部转换允许接收事件并处理动作,然后保持在当前状态。可以显式指定内部转换,例如。

State2 + Event2 / event2_action = State2,

State2 + Event2 / event2_action = _,

或隐式地省略目标状态,包括'='。

State2 + Event2 / event2_action,

还可以定义通配符隐式(或使用'_'显式)内部转换。

statemachine! {
    transitions: {
        *State1 + Event2 = State2,
        State1 + Event3 = State3,
        State1 + Event4 = State4,
        
        _ + Event2 / event2_action,
    },
}

上述示例演示了如何使Event2对任何状态都适用,这些状态都不是之前任何转换所涵盖的,并且对其执行一个动作以进行处理。

这等价于

statemachine! {
    transitions: {
        *State1 + Event2 = State2,
        State1 + Event3 = State3,
        State1 + Event4 = State4,
        
        State2 + Event2 / event2_action = State2,
        State3 + Event2 / event2_action = State3,
        State4 + Event2 / event2_action = State4,
    },
}

请参阅以下测试:test_internal_transition_with_data()test_wildcard_states_and_internal_transitions()以获取使用示例。

守卫表达式

方括号[]中的守卫表达式允许定义多个守卫函数的布尔表达式。例如

statemachine! {
  transitions: {
      *Init + Login(Entry) [valid_entry] / attempt = LoggedIn,
      Init + Login(Entry) [!valid_entry && !too_many_attempts] / attempt = Init,
      Init + Login(Entry) [!valid_entry && too_many_attempts] / attempt = LoginDenied,
      LoggedIn + Logout / reset = Init,
  }
}

守卫表达式可以由守卫函数名称组成,以及它们与&&、||和!运算符的组合。

同一状态和触发事件的多个守卫转换

支持同一状态和触发事件的多个守卫转换(见上述示例)。在这种情况下,假设只有一个守卫被启用,以避免在哪个转换应被选择方面产生冲突。然而,如果有冲突并且多个守卫被启用,将选择在状态机定义中出现的第一个启用的转换。

状态机上下文

状态机需要一个上下文来定义。《StateMachineContext》是由《statemachine!》过程宏生成的,它实现了守卫和动作,以及状态机内部所有状态中都可用并在状态转换之间持续的数据。

statemachine!{
    transitions: {
        State1 + Event1 = State2,
    }
    // ...
}

pub struct Context;

impl StateMachineContext for Context {}

fn main() {
    let mut sm = StateMachine::new(Context);

    // ...
}

请参阅示例 examples/context.rs 以获取使用示例。

状态数据

任何状态都可以与一些数据相关联。

pub struct MyStateData(pub u32);

statemachine!{
    transitions: {
        State1(MyStateData) + Event1 = State2,
    }
    // ...
}

请参阅示例 examples/state_with_data.rs 以获取使用示例。

如果起始状态包含数据,则在创建新机器后必须提供这些数据。

pub struct MyStateData(pub u32);

statemachine!{
    transitions: {
        State2 + Event2 / action = State1(MyStateData),
        *State1(MyStateData) + Event1 = State2,
        // ...
    }
    // ...
}

// ...

let mut sm = StateMachine::new(Context, MyStateData(42));

状态数据还可以具有与它们相关联的生命周期,这些生命周期将由《statemachine!》宏获取并添加到《States》枚举和《StateMachine》结构中。这意味着以下也将工作

pub struct MyStateData<'a>(&'a u32);

statemachine! {
    transitions: {
        *State1 + Event1 / action = State2,
        State2(MyStateData<'a>) + Event2 = State1,
        // ...
    }
    // ...
}

请参阅示例 examples/state_with_reference_data.rs 以获取使用示例。

事件数据

数据可以与事件一起传递到《guard》和《action》中。

pub struct MyEventData(pub u32);

statemachine!{
    transitions: {
        State1 + Event1(MyEventData) [guard] = State2,
    }
    // ...
}

事件数据还可以具有与它们相关联的生命周期,这些生命周期将由《statemachine!》宏获取并添加到《Events》枚举中。这意味着以下也将工作

pub struct MyEventData<'a>(pub &'a u32);

statemachine!{
    transitions: {
        State1 + Event1(MyEventData<'a>) [guard1] = State2,
        State1 + Event2(&'a [u8]) [guard2] = State3,
    }
    // ...
}

请参阅示例 examples/event_with_data.rs 以获取使用示例。

守卫和动作语法

请参阅示例 examples/guard_action_syntax.rs 以获取使用示例。

异步守卫和动作

守卫和动作都可以可选地使用async

use smlang::{async_trait, statemachine};

statemachine! {
    transitions: {
        *State1 + Event1 [guard1] / async action1 = State2,
        State2 + Event2 [async guard2] / action2 = State3,
    }
}


pub struct Context {
    // ...
}

impl StateMachineContext for Context {
    async fn action1(&mut self) -> () {
        // ...
    }

    async fn guard2(&mut self) -> Result<(), ()> {
        // ...
    }

    fn guard1(&mut self) -> Result<(), ()> {
        // ...
    }

    fn action2(&mut self) -> () {
        // ...
    }
}

请参阅示例 examples/async.rs 以获取使用示例。

状态机示例

以下是将UML转换为状态机语言DSL的状态机的一些示例。每个示例的运行版本都可在《examples》文件夹中找到。`.png`文件是用《graphviz》功能生成的。

线性状态机

alt text

DSL实现

statemachine!{
    transitions: {
        *State1 + Event1 = State2,
        State2 + Event2 = State3,
    }
}

此示例在《ex1.rs》中。

循环状态机

alt text

DSL实现

statemachine!{
    transitions: {
        *State1 + Event1 = State2,
        State2 + Event2 = State3,
        State3 + Event3 = State2,
    }
}

此示例在《ex2.rs》中。

使用守卫和动作

alt text

DSL实现

statemachine!{
    transitions: {
        *State1 + Event1 [guard] / action = State2,
    }
}

此示例在《ex3.rs》中。

在转换中使用入口和退出函数

状态机将为所有状态创建一个《on_entry_》和《on_exit_》函数。如果没有使用,它们将被编译器优化掉。一个示例可以在《on_entry_on_exit_generic》中找到。

转换回调

状态机将为每个转换调用转换回调。这个函数将使用旧状态和新状态作为参数被调用。一个例子可以在 dominos 中找到。

辅助工具

自动为状态和事件推导某些特征

derive_eventsderive_states 字段设置为特征数组将为 EventsStates 枚举分别添加一个推导表达式。要推导显示,使用 derive_more::Display

use core::Debug;
use derive_more::Display;
// ...
statemachine!{
    derive_states: [Debug, Display],
    derive_events: [Debug, Display],
    transitions: {
        *State1 + Event1 = State2,
    }
}

// ...

println!("Current state: {}", sm.state().unwrap());
println!("Expected state: {}", States::State1);
println!("Sending event: {}", Events::Event1);

// ...

用于记录事件、守卫、动作和状态转换的钩子

StateMachineContext 特征定义(并提供默认、无操作实现)了对于每个事件、守卫、动作和状态转换被调用的函数。你可以提供自己的实现,以将其插入你首选的日志机制。

fn log_process_event(&self, current_state: &States, event: &Events) {}
fn log_guard(&self, guard: &'static str, result: &Result<(), ()>) {}
fn log_action(&self, action: &'static str) {}
fn log_state_change(&self, new_state: &States) {}

请参阅 examples/state_machine_logger.rs,其中使用了 derive_statesderive_events 推导 Debug 实现,以便于记录。

贡献者

按字母顺序排列的贡献者名单


许可证

根据以下任一许可证授权

由您选择。

依赖关系

~1.5MB
~35K SLoC