2015/09/06

状態遷移smc

組込み系の人間でいるつもりなのだが、状態遷移が苦手だ。
苦手というか、状態遷移図とか表を作るだけ作って、実装はちまちまifとかswitch-caseで分けていたりする。
でも、状態でバーンと動きが変わるのであれば、状態ごとに実施するイベントハンドラを切り替えるようなやり方の方が、実装としてはさっぱりと切り分けられてよいこともある。
まあ、同じような実装をあちこちに書いてしまいがちになるというのはあるがね(下手なだけか?)。


そういった悩みを解決してくれるわけではないのだが、「状態遷移を作るのがめんどくさい」ということについては、多少手助けしてくれるツールがある。

SMC: The State Machine Compiler

これを最初に知ったのは、いまはなきC Magazine誌の記事だった。
株式会社エス・スリー・フォー » SMC(State Map Compiler) の拡張
1999年ってことは、16年前か・・・。
当時の私は、まだインターネットの環境を持ってなかったか、ダイアルアップでちまちまやってた時代だと思う。
ファイルはあるのだが、当時のsmcのバージョンなどはよくわからない。
ただ、bisonとかflexとか、そんなのを使っていたようだ。

確か、お仕事でアプリっぽいものを作っていた時期がある。
そのときにも思い出して、ちょっと調べていたのがこちらだ。
StateMap - YMHRS
7年前か。我ながら適当すぎる・・・が、今とあまり変わらない気がする。
何か変更していたようで、ローカルに持っていた。
が、何をしたかったのかがよくわからん。。。

さて、時代は2015年9月。
smcのバージョンは6.6.0になっている。
bison/flexだったものは、JARファイルで実行できるようになっていた。
そして、対応言語がかなり増えている。
私としては、C言語対応になったのがありがたい。

 

SMCがどういうツールかというと、ルールに沿って状態遷移をテキストに書いておくと、ソースに変換してくれるのだ。
そのくらい自分で書けばいいやん、という気になってしまうかもしれないが、自動で書けそうだ、と思ってしまうと、自動でやらせてしまいたくなるではないか。


C言語でどうやっているか見てみよう。
きっと、ファイルの形式は言語に因らず共通なのだろうけど、C言語でのサンプルだけ見る。

ファイルの先頭は、こう。

%class        AppClass
%header        AppClass.h
%start        Map1::Start
%map        Map1

classとなっているが、これが関数のプレフィクスになるようだ。
あとは、各状態ごとにイベントと遷移先とやることを書いていく。

Start
{
    Zero    Zeros   {}
    One     Ones    {}
    Unknown Error   {}
    EOS     OK      {Acceptable();}
}

"Zero"というイベントを起こすと、状態がZerosに遷移する、という感じだ。
やることは、そのまま関数になるようだ。クラス内の関数を呼ぶ、ということのようで、これにもクラス名のプレフィクスが付いていた。
そうそう、状態を管理する変数としてContext構造体というのを生成してくれるのだが、それも引数に取るようになっている。

void AppClass_Acceptable(struct AppClass *this);

もちろん、呼ぶ関数は自分で実装しないといけないし、プロトタイプ宣言も自分で用意する。
そういうのを書いたヘッダファイルを、%headerで指定するようだ。
ちょっと残念なのは、Windowsで実行しているためかもしれないが、変換した結果のソースファイルを見ると、%headerで指定したファイルを#includeで書いた行がCR/LFで改行されているのだ。
cygwinでやったからLFになると思ったけど、JARだからJavaがそう動いているのかな?

 

さて、呼ぶ方だが、名前にプレフィクスとして%classで指定した名前+"Context_"が付いている。
例えば、上記のZeroイベントであればAppClassContext_Zero()となる。

初期化用にInit()が自動で作られるので、まずはこれを呼ぶ。
のだが、その前に状態を管理する構造体を自作する。名前は%classで指定したもの。
状態として持つべきデータなどは、ここに全部入れておくイメージなのかな?
少なくとも、状態遷移する判断に使うデータはここに入れそうだ。

struct AppClass {
    int isAcceptable;
    struct AppClassContext _fsm;
};

この構造体のインスタンスをInit()の引数で渡す。

struct AppClass thisContext;
AppClassContext_Init(&thisContext._fsm, &thisContext);

「Contextのポインタを渡すんだから第1引数はなくてもいいんじゃないの?」と思ったが、AppClassContextをAppClassの中に入れなくてもよいということだろう。

あとは、イベントが発生したときに関数を呼ぶだけだ。
「関数」と書いたが、デフォルトはマクロで、NO_APPCLASS_SM_MACROマクロを定義すると関数にもできる。

状態遷移するとき、文字列を使っているようなので、状態名がバイナリのサイズに影響しそうだ。
まあ、目くじらを立てるほどではないだろうが、S02みたいな割り切った名前の方がよいかも。

 

gcc -O2で関数化したら、こんな感じだった。

0x00401390                AppClassContext_Init
0x004013c0                AppClassContext_EOS
0x00401400                AppClassContext_One
0x00401440                AppClassContext_Unknown
0x00401480                AppClassContext_Zero

うん、何とも言えんね。

0 件のコメント:

コメントを投稿

コメントありがとうございます。
スパムかもしれない、と私が思ったら、
申し訳ないですが勝手に削除することもあります。