SMC(State Map Compiler) の拡張
はじめに
C Magazine 1999/9 で、SMC(State Map Compiler)を紹介しました。 SMCに状態遷移表を記述したスクリプト(テキストファイル)を食わせると、状態遷移表に書かれた通りに動作するFSM(Finite State Machine:有限状態機械) クラスを生成してくれます。
僕はこのSMCがいたく気に入り、更なる機能拡張を試みました。![]()
スクリプト拡張
まず、SMCに食わすスクリプトの文法を少しばかり拡張しました。
- namespace/package指定
SMCが吐くFSMおよびFSMが参照するコンテキストをnamespaceで囲む機能を追加しました。
スクリプトのヘッダ部に、
namespace <FSM-名前空間> package <FSM-package>
と書いておけば、FSMを指定したnamespaceに置くことができます。
package指定は後述する”Javaコード生成”時の package名を指定します。
同様にコンテキストに対しては、
contextnamespace <Context-名前空間> contextpackage <Context-package>
と記述します。
- 状態が遷移しないイベント
スクリプトの状態遷移部には、
状態 { イベント 次の状態 { アクション } ... }という形式で状態遷移を記述します。状態の遷移を伴わない場合、すなわち’次の状態’が’状態’に等しいときは、
状態 { イベント ----- { アクション } ... }のように、ひとつ以上の’-‘を書くことができます。
- 遷移/アクションが一致するイベント
複数のイベントに対し、まったく同じ遷移/アクションを行なうとき、たとえば、
状態 { イベント-1 次の状態 { アクション } イベント-2 次の状態 { アクション } イベント-3 次の状態 { アクション } ... }のような記述は、
状態 { イベント-1 次の状態 { アクション } イベント-2 = イベント-1 イベント-3 = イベント-1 ... }と書くことで、イベント-2/イベント-3に対する遷移/アクションはイベント-1と同じであることを表現できます。
Javaコード生成
SMCにJavaコードを生成させたのが今回の拡張の目玉です。
fsmname GateFSM
package jp.gr.java_conf.episteme.fsm
namespace fsm
context GateContext
contextpackage jp.gr.java_conf.episteme.ct
contextnamespace ct
initial Locked
{
Locked {
Coin Unlocked Unlock
Pass Locked Alarm
}
Unlocked {
Coin Unlocked ThankYou
Pass Locked Lock
}
}
というスクリプトGateFSM.smをSMCに食わせます:
smc -j GateFSM.sm ... -j : Javaコード生成
すると、smcは以下のようなJavaソース’GateFSM.java’を生成します。
/* produced by SMC-Deluxe */
package jp.gr.java_conf.episteme.fsm;
class GateFSMState {
public String name() { return "(GateFSMState)"; }
public void Pass(GateFSM s) { s.getContext().FSMError("Pass", s.getState().name()); }
public void Coin(GateFSM s) { s.getContext().FSMError("Coin", s.getState().name()); }
};
class GateFSMUnlockedState extends GateFSMState {
// 省略...
};
class GateFSMLockedState extends GateFSMState {
// 省略...
};
public class GateFSM extends jp.gr.java_conf.episteme.ct.GateContext {
public GateFSM() { itsState = LockedState; }
public void Pass() {itsState.Pass(this);}
public void Coin() {itsState.Coin(this);}
public void setState(GateFSMState theState) {itsState=theState;}
public GateFSMState getState() {return itsState;};
public String getStateName() {return itsState.name();};
// 省略...
}
GateFSMのベースクラスjp.gr.java_conf.episteme.ct.GateContextとテストルーチンは以下のようになります。
/*
* GateContext.java
*/
package jp.gr.java_conf.episteme.ct;
public class GateContext {
public void FSMError(String event, String state)
{ System.out.println(state + " 状態で " +
event + " が発生するはずないのに!");
}
public void Lock()
{ System.out.println("ゲートを閉じます"); }
public void ThankYou()
{ System.out.println("これはどうも...有り難く頂戴します"); }
public void Alarm()
{ System.out.println("タダで入っちゃいけませんよ!"); }
public void Unlock()
{ System.out.println("ゲートを開けます"); }
}
/*
* gate.java
*/
import jp.gr.java_conf.episteme.fsm.*;
public class gate {
public static void main(String[] arg) throws Exception {
GateFSM fsm = new GateFSM();
for ( boolean cont = true; cont; ) {
System.out.println("現在の状態は " + fsm.getStateName());
System.out.print("Coin or Pass (c/p) ? ");
int input = System.in.read();
System.in.skip(System.in.available());
switch ( input ) {
case 'c' : fsm.Coin(); break;
case 'p' : fsm.Pass(); break;
default : cont = false;
}
}
}
}
FSMとコンテキストの分離
FSMはコンテキストの派生クラスというオブジェクトモデルではなく、 FSMとコンテキストを独立させるオプション’-s’を追加しました。
smc -s GateFSM.sm ... C++ smc -j -s GateFSM.sm ... Java
によって生成されたコードGateFSM.hおよびGateFSM.javaは以下のようになります。
/*
* GateFSM.h
*/
#include "GateContext.h"
namespace fsm {
...
class GateFSM {
...
private:
GateFSMState* itsState;
ct::GateContext* itsContext;
public:
void setContext(ct::GateContext& context) { itsContext = &context; }
ct::GateContext& getContext() { return *itsContext; }
...
};
}
#endif
/*
* GateFSM.java
*/
package jp.gr.java_conf.episteme.fsm;
...
public class GateFSM {
...
public void setContext(jp.gr.java_conf.episteme.ct.GateContext context)
{ itsContext = context; }
public jp.gr.java_conf.episteme.ct.GateContext getContext()
{ return itsContext; }
}
このように、FSMにメソッドsetContext()が追加されます。
FSMのコンストラクトの後、setContext()によって、コンテキストを設定してください。
実行途中でsetContext()することで、コンテキストをダイナミックに入れ換えることも可能です。
コンテキスト生成
オプション’-c’で、コンテキストにひな型(C++では”コンテキスト名.h” , Javaでは “コンテキスト名.java”)を生成します。
/*
* GateContext.h
*/
#ifndef _H_GateContext
#define _H_GateContext
namespace ct {
class GateContext {
public:
void FSMError(const char*, const char*);
void Lock();
void ThankYou();
void Alarm();
void Unlock();
};
}
#endif
/*
* GateContext.java
*/
package jp.gr.java_conf.episteme.ct;
public class GateContext {
public void FSMError(String event, String state) {
// code here
}
public void Lock() {
// code here
}
public void ThankYou() {
// code here
}
public void Alarm() {
// code here
}
public void Unlock() {
// code here
}
}
抽象コンテキスト
‘-a’オプションでコンテキストが抽象クラスとなります。
ただし、このオプションは’-s’と併用しなければなりません。
C++では全メソッドを純粋仮想関数とします。
/*
* GateContext.h
* (smc -c -a -s GateFSM.sm)
*/
#ifndef _H_GateContext
#define _H_GateContext
namespace ct {
class GateContext {
public:
virtual void FSMError(const char*, const char*) =0;
virtual void Lock() =0;
virtual void ThankYou() =0;
virtual void Alarm() =0;
virtual void Unlock() =0;
};
}
#endif
Javaではinterfaceを定義します。
/*
* GateContext.java
* (smc -j -c -a -s GateFSM.sm)
*/
package jp.gr.java_conf.episteme.ct;
public interface GateContext {
void FSMError(String event, String state);
void Lock();
void ThankYou();
void Alarm();
void Unlock();
}
抽象コンテキストの場合、C++/Javaそれぞれのテストルーチンは以下のようになります。
/*
* gate.cpp
*/
#include "GateContext.h"
#include "GateFSM.h"
#include <iostream>
using namespace std;
class TheGateContext : public ct::GateContext {
public:
virtual void FSMError(const char*, const char*);
virtual void Lock();
virtual void ThankYou();
virtual void Alarm();
virtual void Unlock();
};
void TheGateContext::Lock()
{ cout << "ゲートを閉じます" << endl; }
void TheGateContext::Unlock()
{ cout << "ゲートを開けます" << endl; }
void TheGateContext::Alarm()
{ cout << "タダで入っちゃいけませんよ!" << endl; }
void TheGateContext::ThankYou()
{ cout << "これはどうも...有り難く頂戴します" << endl; }
void TheGateContext::FSMError(const char* event, const char* state) {
cout << state << " 状態で "
<< event << " が発生するはずないのに!"
<< endl;
}
int main() {
TheGateContext context;
fsm::GateFSM fsm;
fsm.setContext(context);
for ( bool cont = true; cont; ) {
char input[32];
cout << "現在の状態は : " << fsm.getState().name() << endl;
cout << "Coin or Pass (c/p) ? " << flush;
cin >> input;
switch ( *input ) {
case 'c' : fsm.Coin(); break;
case 'p' : fsm.Pass(); break;
default : cont = false;
}
}
return 0;
}
/*
* gate.java
*/
import jp.gr.java_conf.episteme.fsm.*;
import jp.gr.java_conf.episteme.ct.*;
class TheGateContext implements GateContext {
public void FSMError(String event, String state) {
System.out.println(state + " 状態で " +
event + " が発生するはずないのに!");
}
public void Lock()
{ System.out.println("ゲートを閉じます"); }
public void ThankYou()
{ System.out.println("これはどうも...有り難く頂戴します"); }
public void Alarm()
{ System.out.println("タダで入っちゃいけませんよ!"); }
public void Unlock()
{ System.out.println("ゲートを開けます"); }
}
public class gate {
public static void main(String[] arg) throws Exception {
GateFSM fsm = new GateFSM();
GateContext context = new TheGateContext();
fsm.setContext(context);
for ( boolean cont = true; cont; ) {
System.out.println("現在の状態は " + fsm.getStateName());
System.out.print("Coin or Pass (c/p) ? ");
int input = System.in.read();
System.in.skip(System.in.available());
switch ( input ) {
case 'c' : fsm.Coin(); break;
case 'p' : fsm.Pass(); break;
default : cont = false;
}
}
}
}