#有限状态机 #fsm #cpp #状态机 #代码生成

app fsm_gen

FSM(有限状态机)代码生成器(目前为C++)

6个版本

0.6.1 2020年11月2日
0.6.0 2020年11月2日
0.2.0 2019年3月31日
0.1.2 2019年3月23日

#748 in 开发工具

每月 25 次下载

GPL-3.0 许可协议

110KB
2.5K SLoC

C++ 1.5K SLoC // 0.0% comments Rust 830 SLoC // 0.0% comments Prolog 26 SLoC

FSM生成器

安装

cargo install -f fsm_gen

您可以从仓库下载并像往常一样构建和安装

cargo build --release
cargo install --path . -f

版本

0.6

  • cpp 生成代码,可选转换为 error 状态
  • 现在我们谈论的是 模板 而不是 语言

0.5

  • 迁移到 tera 模板
  • 添加了dot文件生成

0.4

  • 在C++上重新定义了错误动作(查看示例iceberg)
  • 修复了在当前目录运行时的错误
  • 为生成的.h和.cpp文件添加了完整的建议文件

0.3

  • 多行守卫没有状态名称
  • 多守卫和多动作
  • 输入中的'_'表示任何其他输入
  • 负守卫
  • 如果没有转换,它将生成一个错误
  • 更新rust-peg库并使用宏
  • 如果抛出异常,我们将进入错误转换
  • 更好的错误信息
    • 在解析时显示行号
    • 检查孤儿状态
  • makefile安装,测试cpp

0.2

  • 动作
  • 私有hpp(手动文件)
  • 模板函数,用于专门处理事务变化
  • 函数的匿名 namespace
  • 在fsm语法上添加了注释支持

待办事项

  • 错误转换是特殊的

  • 改进异常控制

  • 模板的web界面

  • 从文件读取模板

  • 更新idata以使用ispush和btrees等

  • 完成cpp示例并在README.md上更新

  • 检查fsm格式

    • 检测重复状态
    • 检测重复输入和守卫
  • 输出带有信号

  • 添加模板

目标

所有计算过程都由接收输入并通过生成输出进行处理组成。从简单到复杂。

有时,过程取决于上下文,并且需要管理状态。

因此,消息和状态管理是任何软件过程的基本元素。

这就是为什么我很早就为这两个元素编写了两个外部DSL(我在生产中仍在使用它们,并且它们非常有帮助)。

在这个存储库中,我重写了一个,即状态机的代码生成器。

目前它生成C++代码(我在生产中最紧迫的目标)。

您可以在输入结构体和每个状态上都有数据(字段)

为了解释系统,我将使用cpp_test/fsm中的示例。

此示例是关于编写一个处理登录请求的系统的。

首先,服务器会被要求提供密码。

这个密钥将用于在登录请求中编码用户名和密码(这个将通过哈希函数传递)。

这种编码将是不可逆的(哈希函数)。服务器将执行相同的操作(从密码的哈希值开始)来验证其有效性。

如果一切正常,它将发送一个登录确认。

图表看起来是这样的

Basic diagram

可以写出一系列转换

//  Example of fsm to manage login
//  on server side

[init]
    rq_key          ->  w_login     /   send_key
    timer           ->  init

[w_login]
    rq_login        ->  login       /   send_login
    timer
        & timeout   ->  error
                    ->  w_login
[login]
    rq_logout       ->  logout      /   send_logout
    heartbeat       ->  login       /   update_hb
    timer
        & timeout   ->  logout
                    ->  login

[logout]
    timer           ->  logout

[error]
    _               ->  error

这是此工具生成代码的输入

实际上,之前的图表也是从这个DSL生成的(它生成了一个graphviz dot文件)

元素

状态

[init]
    ...

initw_loginlogin ... 是状态

根据输入和状态(稍后将看到其值),系统将转换到新状态。

转换

    rq_key                  ->  w_login

如果我们收到一个输入(在这种情况下为rq_key),我们将转到下一个状态(w_login

输入

状态机接收到的元素。

    INPUT
      v
    rq_key                  ->  w_login

在示例中,它们是 rq_keyrq_loginrq_logoutheartbeattimer

守卫

根据状态和输入调用的函数,以决定前进的方式。

                    GUARD
                     v
    rq_login    &   valid   ->  login       /   send_login

你可以有多个守卫

    rq_login    &   valid  &  guard2  ->  login       /   send_login

当你应用不同的选项守卫(或组合)...

在示例中,我们有 validtimeoutontime

    rq_login
        &   valid   ->  login       /   send_login
                    ->  logout      /   log_err

可以用负守卫来写

    rq_login
        &   !valid  ->  logout      /   log_err
                    ->  login       /   send_login

最终状态

->箭头后面是我们将转换到的状态。

                               FINAL_STATUS
                                   v
    rq_key                  ->  w_login

动作

我们可以在执行事务时定义要执行的操作。

这将在最终状态和'/'之后。

                                                ACTION
                                                  v
    rq_login                ->  logout      /   log_err

你可以有多个操作

    rq_login                ->  logout      /   log_err  action2

在这个例子中,我们有 send_keysend_login...

特殊转换

在所有状态下,都必须考虑所有输入。

但是,许多转换是相同的(通常是错误情况)。

这用输入 _ 标记

考虑这个状态示例

[init]
    rq_key                      ->  w_login     /   send_key
    timer                       ->  init
    _                           ->  logout      /   log_err

_ 表示 ... 任何其他输入 ...

因此,考虑在此状态中所有可能的输入

error 状态和隐式转换

error 是一个特殊状态

你可以显式地移动到 error 状态。

任何未定义的转换都将最终结束在错误状态。

你还可以在转换函数上放置一些验证,并在失败的情况下移动到错误状态(即使它没有在 fsm 上显式编写)

这是因为检查参数如此常见,添加守卫会产生很多噪音

在我们的例子...

[init]
    rq_key          ->  w_login     /   send_key
    timer           ->  init

对于 rq_loginrq_logout 没有转换。两者都是隐式的,相当于 ...

[init]
    rq_key          ->  w_login     /   send_key
    timer           ->  init
    _               ->  error

这是转换函数控制(当然你可以按需专门化)

    //  status change functions
    template <typename FROM, typename IN, typename TO>
    std::variant<TO, st_error_t> fromin2(const FROM &, const IN &) {
        //  here you could check the params and decide to go to error
        //  instead to the programmed trasnsition
        ...
    }

这是当你显式编写一个最终结束在错误状态的转换时

    template <typename FROM, typename IN>
    st_error_t fromin2error(const FROM &, const IN &) {
      return st_error_t{...};
    }

结束错误转换的另一种方法。如果在处理输入时抛出异常...

  [...]
  } catch (...) {}

  auto nw_st_info = fromin2error<st_init_t, in_rq_key_t>(this->info, in);
  log("[init] rq_key error/default -> error", in, info, nw_st_info);
  return std::make_shared<error>(nw_st_info);

注释

注释从//开始,并持续到行尾

用法

要获取帮助...

fsm_gen --help
> fsm_gen -h

fsm_gen 0.6.1
jleahred

    Generate code from a simple fsm file
    To check the supported templates  --show_templs
    

USAGE:
    fsm_gen [FLAGS] [OPTIONS] [fsm_files]...

FLAGS:
    -d, --dot-graphviz    Generate graphviz dot file
    -h, --help            Prints help information
        --help-cpp        Give me some information about generating cpp files
    -s, --show-templs     Show supported template generators
    -V, --version         Prints version information

OPTIONS:
    -T, --threads <n_threads>    Number of threads to use. 0 means one per core  ;-) [default: 0]
    -t, --templ <templ>          Template to generate code (show available --show-templs) [default: cpp]

ARGS:
    <fsm_files>...    List of fsm files to be processed

默认模板是 c++(目前只有这一个)

你可以运行

fsm_gen login.fsm

它将生成 c++

您可以传递一个fsm文件的列表

fsm_gen login.fsm  test/seller.fsm test/test2/lift.fsm

代码将在原始.fsm文件的同一目录下生成

如果您的shell支持它,您可以运行...

fsm_gen **/*.fsm

C++代码生成

从示例login.fsm开始,系统将创建...

    fsm_login_gen.h
    fsm_login_gen.cpp

您不需要修改这些文件。

您必须在这些文件上编写您的代码...

    fsm_login_types.h
    fsm_login_private.hpp

如果不存在,这两个文件将作为参考创建

这两个文件的空参考代码将以注释的形式添加到_gen文件中。

文件依赖关系

cpp_files_depen

fsm_login_gen.h

完整代码在cpp_test/fsm/fsm_login_gen.h

我们得知它是在何时创建的。

此文件是使用或扩展程序中生成的状态机的起点。

//  generated automatically  2019-03-22 11:24:40
//  do not modify it manually

根据文件名创建头文件和namespaces

#pragma once

#include <iostream>
#include <memory>

namespace login {

内部类前置声明

class BaseState;
typedef std::shared_ptr<BaseState> SState;

您需要实例化或扩展的fsm

//  -------------------
//      F S M
class Fsm {
public:
  Fsm();
  ~Fsm();

  void process(const heartbeat_t& in);
  void process(const rq_key_t& in);
  void process(const rq_login_t& in);
  void process(const rq_logout_t& in);
  void process(const timer_t& in);

  ...
}

fsm_login_gen.cpp

完整代码在cpp_test/fsm/fsm_login_gen.cpp

您必须不修改此文件,并且不需要了解很多关于它的信息

fsm_login_types.h

完整代码在cpp_test/fsm/fsm_login_types.h

在此文件中,您必须声明状态信息类型和输入类型

如果该文件不存在,它将以空数据类型创建

//  Code generated automatically to be filled manually
//  This file will not be updated by generator
//  It's created just the first time as a reference
//  Generator will allways create a  .reference file to help with
//  new methods and so

基于fsm文件的namespace,我们有两种类型要声明

如果需要,您当然可以在cpp文件中完成这些类型

namespace login {

  //  status info types
  struct st_init_t{};
  struct st_w_login_t{};
  struct st_login_t{};
  struct st_logout_t{};
  struct st_error_t{};


  //  input types
  struct in_heartbeat_t {};
  struct in_rq_key_t {};
  struct in_rq_login_t {};
  struct in_rq_logout_t {};
  struct in_timer_t {};


} // namespace login
#endif // FSM_LOGIN_H

fsm_login_private.hpp

完整代码在cpp_test/fsm/fsm_login_private.hpp

这是您必须手动维护的另一文件。


//  Code generated automatically to be filled manually
//  This file will not be updated by generator
//  It's created just the first time as a reference
//  Generator will allways create a  .reference file to help with
//  new methods and so

//  This file will be included in _gen.cpp
//  (anywhere else)

//  to make happy some IDEs
#include "fsm_login_types.h"
#include "fsm_login_gen.h"

namespace {
    using namespace login;

如您所见,它在匿名namespace中。

此文件是私有的,它将被_gen.cpp包含。定义一个匿名namespace,我们保持此实现为私有。更重要的是,如果忘记了一个实现,或者一个实现是不必要的,编译器会提醒我们。

日志和状态转换是模板。

这是一个日志示例

[init] rq_key -> w_login
[w_login] rq_login(valid) -> login
[login] rq_logout -> logout

这样,您可以根据需要专门化或泛化。

    //  log
    template <typename IN, typename INIT_ST, typename END_ST>
    void log(const std::string &txt_trans, const IN &, const INIT_ST &,
            const END_ST &) {
        std::cout << txt_trans << std::endl;
    }

    //  status change functions
    template <typename FROM, typename IN, typename TO>
    std::variant<TO, st_error_t> fromin2(const FROM &, const IN &) {
        //  you can specialize this generic function
        return TO{};
    }
    template <typename FROM, typename IN>
    st_error_t fromin2error(const FROM &, const IN &) {
      return st_error_t{};
    }

日志的第一个参数是一个字符串,包含转换更改信息(初始转换、输入、如果有守卫,则最终转换)

接下来,我们有守卫和操作函数。

    //  guards
    bool valid(const in_rq_login_t& /*in*/, const  st_w_login_t& /*st_info*/) { return true; }
    bool timeout(const in_timer_t& /*in*/, const  st_w_login_t& /*st_info*/) { return true; }
    bool timeout(const in_timer_t& /*in*/, const  st_login_t& /*st_info*/) { return true; }


    //  actions
    void act_send_key(const st_init_t& /*st_orig*/, const in_rq_key_t& /*in*/, const  st_w_login_t& /*st_dest*/) {}
    void act_send_login(const st_w_login_t& /*st_orig*/, const in_rq_login_t& /*in*/, const  st_login_t& /*st_dest*/) {}
    void act_send_logout(const st_login_t& /*st_orig*/, const in_rq_logout_t& /*in*/, const  st_logout_t& /*st_dest*/) {}
    void act_update_hb(const st_login_t& /*st_orig*/, const in_heartbeat_t& /*in*/, const  st_login_t& /*st_dest*/) {}


} // namespace anonymous

图表源

digraph G {
rankdir = BT;
  node [shape=plaintext fontname="Arial"];

main -> "fsm_login_gen.h"

"fsm_login_gen.cpp" -> "fsm_login_gen.h"

"fsm_login_gen.h" -> "fsm_login_types.h"

}

依赖关系

~10-21MB
~270K SLoC