SMC(The State Machine Compiler)を試す その2

その1では、テキストで書いた状態遷移マップから
Cのコードを生成し、実行してみました。

今回は実際に生成されたコードを読んでみたいと思います。

ライセンス
生成されたコードのライセンスは?

FAQに記載がありました。

 Does the SMC license cover the files SMC generates? 
No. The .sm files and the output files SMC generates from .sm files belong to you and are covered by your copyright (if you are using one.) 

とあるので、
Q.SMCのライセンスは、SMCが生成するファイルをカバーしますか?
A. .smファイル、およびSMCが.smファイルから生成する出力ファイルは、
あなたが所有しており、あなたの著作権によってカバーされます。

ご自由にどうぞと読めます。


思い込み

生成コードを想像してみました。
ぱっと思いつくのは状態を変数でもち、イベント判定をして、
代入によって遷移を行なうコードです。

 switch(状態変数)
 {
   case 状態1:
     if(イベント1発生()) {
       状態変数 = 状態2;
     }
   break;
   case 状態2:
     if(イベント2発生()) {
       状態変数 = 状態1;
     }
   break;
 }

この想像はまったく役に立ちませんでした。
状態変数の定義もないし、状態を判定している
処理もありません。
ではsmcはどうやって状態遷移を実現しているのでしょうか。


ファイル

ユーザーが書くコードのサンプル

  • AppClass.c
  • AppClass.h
  • main.c

SMCにより提供されたヘッダファイル

  • ../../../lib/C/statemap.h

SMCが生成したコード

  • AppClass_sm.c
  • AppClass_sm.h

デバッグコード

コード生成したときに-gオプションを付けていると
トレース用のデバッグコードが埋め込まれて、読みにくいので
オプション(-g)を外して生成してみます。

オプション有り
%wc *.[ch]
  401   967 10783 AppClass_sm.c
   77   160  1721 AppClass_sm.h

オプション無し
%wc *.[ch]
  254   487  5892 AppClass_sm.c
   68   133  1415 AppClass_sm.h

ぐっとコード量が減りました。
これで読みやすくなりました。

関数群
はじめにtagsファイルを作ってみました。

appclass.c(48) : void AppClass_Init(struct AppClass *this)
appclass.c(58) : void AppClass_Acceptable(struct AppClass *this)
appclass.c(63) : void AppClass_Unacceptable(struct AppClass *this)
appclass.c(68) : int AppClass_CheckString(struct AppClass *this, const char *theString)
appclass_sm.c(29) : static void AppClassState_EOS(struct AppClassContext *fsm)
appclass_sm.c(34) : static void AppClassState_One(struct AppClassContext *fsm)
appclass_sm.c(39) : static void AppClassState_Unknown(struct AppClassContext *fsm)
appclass_sm.c(44) : static void AppClassState_Zero(struct AppClassContext *fsm)
appclass_sm.c(49) : static void AppClassState_Default(struct AppClassContext *fsm)
appclass_sm.c(95) : static void Map1_Start_EOS(struct AppClassContext *fsm)
appclass_sm.c(107) : static void Map1_Start_One(struct AppClassContext *fsm)
appclass_sm.c(116) : static void Map1_Start_Unknown(struct AppClassContext *fsm)
appclass_sm.c(125) : static void Map1_Start_Zero(struct AppClassContext *fsm)
appclass_sm.c(136) : static void Map1_Zeros_EOS(struct AppClassContext *fsm)
appclass_sm.c(148) : static void Map1_Zeros_One(struct AppClassContext *fsm)
appclass_sm.c(157) : static void Map1_Zeros_Unknown(struct AppClassContext *fsm)
appclass_sm.c(166) : static void Map1_Zeros_Zero(struct AppClassContext *fsm)
appclass_sm.c(177) : static void Map1_Ones_EOS(struct AppClassContext *fsm)
appclass_sm.c(189) : static void Map1_Ones_One(struct AppClassContext *fsm)
appclass_sm.c(198) : static void Map1_Ones_Unknown(struct AppClassContext *fsm)
appclass_sm.c(207) : static void Map1_Ones_Zero(struct AppClassContext *fsm)
appclass_sm.c(220) : static void Map1_Error_EOS(struct AppClassContext *fsm)
appclass_sm.c(231) : static void Map1_Error_One(struct AppClassContext *fsm)
appclass_sm.c(237) : static void Map1_Error_Unknown(struct AppClassContext *fsm)
appclass_sm.c(243) : static void Map1_Error_Zero(struct AppClassContext *fsm)
main.c(46) : int main(int argc, char *argv[])

なんとなく分かってしまいましたかね。

Map1_Start_One()は Start状態で、Oneイベントが
発生したときに実行される関数です。
同じルールで「状態の数 x イベントの数」分の関数が生成されています。

関数の中身は

  1. .EXIT_STATE 状態から出て行く処理
  2. .AppClass_Acceptable() イベント処理
  3. .setState() 遷移処理
  4. .ENTRY_STATE 新しい状態に入った処理

遷移先の情報はこれらの関数が持っています。


状態発見

生成されたコードを眺めていると状態を見つけました。

AppClass_sm.c(133): const struct AppClassState Map1_Start = { POPULATE_STATE(Map1_Start), 0 };
AppClass_sm.c(174): const struct AppClassState Map1_Zeros = { POPULATE_STATE(Map1_Zeros), 1 };
AppClass_sm.c(215): const struct AppClassState Map1_Ones = { POPULATE_STATE(Map1_Ones), 2 };
AppClass_sm.c(217): const struct AppClassState Map1_OK = { POPULATE_STATE(Map1_OK), 3 };
AppClass_sm.c(248): const struct AppClassState Map1_Error = { POPULATE_STATE(Map1_Error), 4 };

状態は変数ではなく、定数でROMコード上に存在しました。

マクロ展開してみましょう。

const struct AppClassState Map1_Start = { 
 Map1_Start_EOS, 
 Map1_Start_One, 
 Map1_Start_Unknown, 
 Map1_Start_Zero, 
 AppClassState_Default,
 0 };

状態は、関数へのポインタを持ちます。
何を指すのか?
先ほど「状態の数 x イベントの数」と書いた
遷移用の関数へのポインタを持ちます。

AppClassState構造体の定義を見ると、
間違い無いことが分かります。

struct AppClassState
{

    void(*EOS)(struct AppClassContext*);
    void(*One)(struct AppClassContext*);
    void(*Unknown)(struct AppClassContext*);
    void(*Zero)(struct AppClassContext*);

    void(*Default)(struct AppClassContext*);
    STATE_MEMBERS
};

重要なのでもう1度。
状態はROMコード上に存在し、遷移を持ちます。
遷移は状態+アクション用の関数へのポインタで表現します。

では全体の流れを把握するために
main.cから見てみましょう。

int main(int argc, char *argv[])
{
	struct AppClass thisContext;

どうやら thisContext が現在の状態を保存する
変数のようです。AppClassの型を見てみましょう。

struct AppClass
{
	/* If a string is acceptable, then this variable is set to YES;
	 * NO, otherwise.
	 */
	int isAcceptable;

	struct AppClassContext _fsm;
};

さらに深追いし、AppClassContextの定義を見ます。

struct AppClassContext
{
    struct AppClass *_owner;
    FSM_MEMBERS(AppClass)
};

*_ownerはポインタ変数でした。

なるほど、状態を格納した変数が存在しなかったのはポインタ変数
で表現されているからなのでした。

ではmain.cに戻ります。

main.cの主な処理は

  1. .初期化 AppClass_Init()
  2. .状態遷移 AppClass_CheckString()

です。

AppClass.c内に記述された
AppClass_CheckString()を見てみます。

int AppClass_CheckString(struct AppClass *this, const char *theString)
{
	while (*theString)
	{
		switch (*theString)
		{
		case '0':
			AppClassContext_Zero(&this->_fsm);
			break;

		case '1':
			AppClassContext_One(&this->_fsm);
			break;

		default:
			AppClassContext_Unknown(&this->_fsm);
			break;
		}
		++theString;
	}

	/* end of string has been reached - send the EOS transition. */
	AppClassContext_EOS(&this->_fsm);

	return this->isAcceptable;
}

文字列が終端にくるまで繰り返し、イベントの判定をしています。
0だったらAppClassContext_Zero()
1だったらAppClassContext_One()
それ以外だったらAppClassContext_Unknown()
を実行。
ここがイベントの判定と実行です。

AppClassContext_Zero()はAppClass_sm.hで定義された
マクロです。

#define AppClassContext_Zero(fsm) \
    assert(getState(fsm) != NULL); \
    getState(fsm)->Zero(fsm);
<<

分かりづらいのでプリプロセスを通したあとのコードを見ましょう。


>||
gcc -E -I../../../lib/C AppClass_sm.c AppClass.c main.c

プリプロセス前: getState(fsm)->Zero(fsm);
プリプロセス後: (&this->_fsm)->_state->Zero(&this->_fsm);

ゼロを見つけたときに実行した
AppClassContext_Zero()
は、現在の状態(ROMコード上へのポインタ)のもつ
Zero関数ポインタが示す関数を実行するのでした。

Zero()イベントが成立したとき
Start状態の場合は、Map1_Start_Zero()を実行
Zeros状態の場合は、Map1_Zeros_Zero()を実行
Ones状態の場合は、Map1_Ones_Zero()を実行
Error状態の場合は、Map1_Error_Zero()を実行

Zero()成立時、わざわざ状態を判定しなくても良いのが良いですね。


おさらい。

smcが生成するCのコードは、

  • 状態 x イベント分の関数を生成し、遷移先を記述する。
  • ROMコード上に状態つくり、遷移は関数テーブルで格納する。
  • 現在状態はROMコード上の状態へのポインタで表現する。
  • 遷移は、現在の状態のもつイベント(関数ポインタ)を実行する。

ことで実現する。

最初は分かりづらいコードだと思いましたが
ROMコード上に、実際に状態が存在し、ポインタで
現在状態を示すのは実世界を具現化しているようで
面白いコードだと感じました。

次回は他言語でもコード生成を行なってみます。