私は、CとC++が混在した小さなプロジェクトを作成しています。私は、ワーカースレッドの中心に小さな状態のマシンを1つ構築しています。
SOの達人がステートマシンの設計手法を共有してくれるかどうか疑問に思っていました。
注:私は主に実装技術を試し、テストした後です。
UPDATED:SOで収集されたすべての素晴らしい入力に基づいて、私はこのアーキテクチャに落ち着きました:
以前に設計したステートマシン(C++ではなくC)は、すべてstruct
配列とループになりました。構造は基本的に、状態とイベント(ルックアップ用)、および次のような新しい状態を返す関数で構成されます。
typedef struct {
int st;
int ev;
int (*fn)(void);
} tTransition;
次に、単純な定義で状態とイベントを定義します(ANY
のものは特別なマーカーです。以下を参照):
#define ST_ANY -1
#define ST_INIT 0
#define ST_ERROR 1
#define ST_TERM 2
: :
#define EV_ANY -1
#define EV_KEYPRESS 5000
#define EV_MOUSEMOVE 5001
次に、遷移によって呼び出されるすべての関数を定義します。
static int GotKey (void) { ... };
static int FsmError (void) { ... };
これらの関数はすべて、変数を受け取らず、ステートマシンの新しい状態を返すように記述されています。この例では、必要に応じて状態関数に情報を渡すためにグローバル変数が使用されます。
FSMは通常単一のコンパイルユニット内でロックされ、すべての変数はそのユニットに対して静的であるため、グローバルの使用は見た目ほど悪くありません(これが上記の「グローバル」の周りに引用符を使用した理由です。 FSM、真にグローバルよりも)。すべてのグローバルと同様に、注意が必要です。
次に、transitions配列は、考えられるすべての遷移と、それらの遷移に対して呼び出される関数(最後のすべてを含む)を定義します。
tTransition trans[] = {
{ ST_INIT, EV_KEYPRESS, &GotKey},
: :
{ ST_ANY, EV_ANY, &FsmError}
};
#define TRANS_COUNT (sizeof(trans)/sizeof(*trans))
つまり、ST_INIT
状態にあり、EV_KEYPRESS
イベントを受け取った場合、GotKey
を呼び出します。
FSMの動作は、比較的単純なループになります。
state = ST_INIT;
while (state != ST_TERM) {
event = GetNextEvent();
for (i = 0; i < TRANS_COUNT; i++) {
if ((state == trans[i].st) || (ST_ANY == trans[i].st)) {
if ((event == trans[i].ev) || (EV_ANY == trans[i].ev)) {
state = (trans[i].fn)();
break;
}
}
}
}
上記で示唆したように、ST_ANY
をワイルドカードとして使用し、現在の状態に関係なくイベントが関数を呼び出すことができることに注意してください。 EV_ANY
も同様に機能し、特定の状態のイベントが関数を呼び出すことを許可します。
また、transitions配列の最後に到達した場合、FSMが正しく構築されていないことを示すエラーが表示されることを保証できます(ST_ANY/EV_ANY
の組み合わせを使用します)。
組み込みシステムの通信スタックやプロトコルの初期実装など、非常に多くの通信プロジェクトでこれに似たコードを使用しました。大きな利点は、そのシンプルさと、トランジションの配列を比較的簡単に変更できることでした。
今日、より適切な高レベルの抽象化が存在することは間違いありませんが、それらはすべてこの同じ種類の構造に要約されると思われます。
また、コメントでldog
が示すように、すべての関数に構造体のポインターを渡す(およびイベントループでそれを使用する)ことで、グローバルを完全に回避できます。これにより、複数のステートマシンが干渉なしで並行して実行できるようになります。
マシン固有のデータ(最低限の状態)を保持する構造タイプを作成し、グローバルの代わりにそれを使用するだけです。
私がめったにそれをやったことがない理由は、私が書いたステートマシンのほとんどがシングルトンタイプ(たとえば、1回限り、プロセス開始、設定ファイルの読み取り)であり、複数のインスタンスを実行する必要がないためです。ただし、複数実行する必要がある場合には価値があります。
他の答えは良いですが、ステートマシンが非常に単純なときに使用した非常に「軽量」な実装は次のようになります。
enum state { ST_NEW, ST_OPEN, ST_SHIFT, ST_END };
enum state current_state = ST_NEW;
while (current_state != ST_END)
{
input = get_input();
switch (current_state)
{
case ST_NEW:
/* Do something with input and set current_state */
break;
case ST_OPEN:
/* Do something different and set current_state */
break;
/* ... etc ... */
}
}
ステートマシンが十分に単純で、関数ポインターと状態遷移テーブルのアプローチが過剰である場合に使用します。これは、文字ごとまたは単語ごとの解析に役立ちます。
コンピューターサイエンスのすべての規則を破って申し訳ありませんが、ステートマシンはgoto
ステートメントのほうが効率的であるだけでなく、コードをよりクリーンにし、読みやすい。 goto
ステートメントはラベルに基づいているため、大量の数字を追跡したり、enumを使用したりする代わりに、状態に名前を付けることができます。また、関数ポインターや巨大なswitchステートメントやwhileループの余分な部分をすべて必要としないため、コードがずっときれいになります。私もそれがより効率的だと言いましたか?
ステートマシンは次のようになります。
void state_machine() {
first_state:
// Do some stuff here
switch(some_var) {
case 0:
goto first_state;
case 1:
goto second_state;
default:
return;
}
second_state:
// Do some stuff here
switch(some_var) {
case 0:
goto first_state;
case 1:
goto second_state;
default:
return;
}
}
一般的なアイデアが得られます。重要なのは、ステートマシンを効率的な方法で実装でき、比較的読みやすく、ステートマシンを見ていることを読者に叫ぶ方法です。 goto
ステートメントを使用している場合は、気をつけてください。そうすると、自分で足を撃ちやすくなります。
ステートマシンコンパイラを検討することもできます http://smc.sourceforge.net/
このすばらしいオープンソースユーティリティは、ステートマシンの記述を単純な言語で受け取り、CやC++を含む12以上の言語のいずれかにコンパイルします。ユーティリティ自体はJavaで記述されており、ビルドの一部として含めることができます。
これを行う理由は、GoF Stateパターンまたはその他のアプローチを使用して手動でコーディングするのではなく、ステートマシンがコードとして表現されると、それをサポートするために生成する必要のある定型的な重みの下で、基礎となる構造が消える傾向があるためです。このアプローチを使用すると、関心事を優れた方法で分離でき、ステートマシンの構造を「見える」状態に保ちます。自動生成されたコードは、触れる必要のないモジュールに組み込まれるため、記述したサポートコードに影響を与えることなく、ステートマシンの構造に戻ることができます。
申し訳ありませんが、私は熱心であり、間違いなく皆を先送りしています。しかし、これは一流のユーティリティであり、十分に文書化されています。
Miro Samek(ブログ State Space 、ウェブサイト State Machines&Tools )の作業を確認してください。C/C++ユーザーの記事Journalは素晴らしかった。
Webサイトには、ステートマシンフレームワーク(QPフレームワーク)、イベントハンドラー(QEP)、基本モデリングツール(QM)および状態マシンの描画、コードの作成、デバッグを可能にするトレースツール(QSpy)。
この本には、実装の理由/理由とその使用方法に関する広範な説明が含まれており、階層型および有限状態マシンの基礎を理解するための優れた資料でもあります。
このWebサイトには、組み込みプラットフォームでソフトウェアを使用するためのいくつかのボードサポートパッケージへのリンクも含まれています。
状態/イベント遷移の配列の代わりに、paxdiabloが説明するものと同様のことを行いました。関数ポインターの2次元配列を設定し、イベント値を1つの軸のインデックスとして、現在の状態値をもう一方。次に、state = state_table[event][state](params)
を呼び出すだけで、正しいことが起こります。無効な状態/イベントの組み合わせを表すセルは、もちろんそういう関数へのポインターを取得します。
明らかに、これは、状態とイベントの値が両方とも連続した範囲であり、0から始まるか、十分に近い場合にのみ機能します。
非常に素晴らしいテンプレートベースのC++ステートマシン「フレームワーク」は、Stefan Heinzmannの article で提供されています。
記事には完全なコードダウンロードへのリンクがないため、自由にコードをプロジェクトに貼り付けてチェックアウトしました。以下のものはテストされており、いくつかのマイナーではありますが、明らかに欠落している部分が含まれています。
ここでの主要な革新は、コンパイラが非常に効率的なコードを生成していることです。空の入退場アクションには費用はかかりません。空ではない入場/退場アクションはインライン化されます。コンパイラは、ステートチャートの完全性も検証しています。欠落しているアクションはリンクエラーを生成します。キャッチされない唯一のものは、欠落しているTop::init
です。
これはMiro Samekの実装の非常に良い代替物です。欠けているものなしで生きることができれば-これは完全なUML Statechart実装とは程遠いですが、UMLセマンティクスを正しく実装しますが、Samekのコードは終了/遷移を処理しません/ entryアクションが正しい順序で。
このコードが必要なことに対して機能し、システムに適切なC++コンパイラがあれば、おそらくMiroのC/C++実装よりもパフォーマンスが向上します。コンパイラは、フラット化されたO(1)遷移状態マシン実装を生成します。アセンブリ出力の監査により、最適化が希望どおりに機能することが確認されると、理論上のパフォーマンスに近づきます。最良の部分:比較的小さく、理解しやすいコードです。
#ifndef HSM_HPP
#define HSM_HPP
// This code is from:
// Yet Another Hierarchical State Machine
// by Stefan Heinzmann
// Overload issue 64 december 2004
// http://accu.org/index.php/journals/252
/* This is a basic implementation of UML Statecharts.
* The key observation is that the machine can only
* be in a leaf state at any given time. The composite
* states are only traversed, never final.
* Only the leaf states are ever instantiated. The composite
* states are only mechanisms used to generate code. They are
* never instantiated.
*/
// Helpers
// A gadget from Herb Sutter's GotW #71 -- depends on SFINAE
template<class D, class B>
class IsDerivedFrom {
class Yes { char a[1]; };
class No { char a[10]; };
static Yes Test(B*); // undefined
static No Test(...); // undefined
public:
enum { Res = sizeof(Test(static_cast<D*>(0))) == sizeof(Yes) ? 1 : 0 };
};
template<bool> class Bool {};
// Top State, Composite State and Leaf State
template <typename H>
struct TopState {
typedef H Host;
typedef void Base;
virtual void handler(Host&) const = 0;
virtual unsigned getId() const = 0;
};
template <typename H, unsigned id, typename B>
struct CompState;
template <typename H, unsigned id, typename B = CompState<H, 0, TopState<H> > >
struct CompState : B {
typedef B Base;
typedef CompState<H, id, Base> This;
template <typename X> void handle(H& h, const X& x) const { Base::handle(h, x); }
static void init(H&); // no implementation
static void entry(H&) {}
static void exit(H&) {}
};
template <typename H>
struct CompState<H, 0, TopState<H> > : TopState<H> {
typedef TopState<H> Base;
typedef CompState<H, 0, Base> This;
template <typename X> void handle(H&, const X&) const {}
static void init(H&); // no implementation
static void entry(H&) {}
static void exit(H&) {}
};
template <typename H, unsigned id, typename B = CompState<H, 0, TopState<H> > >
struct LeafState : B {
typedef H Host;
typedef B Base;
typedef LeafState<H, id, Base> This;
template <typename X> void handle(H& h, const X& x) const { Base::handle(h, x); }
virtual void handler(H& h) const { handle(h, *this); }
virtual unsigned getId() const { return id; }
static void init(H& h) { h.next(obj); } // don't specialize this
static void entry(H&) {}
static void exit(H&) {}
static const LeafState obj; // only the leaf states have instances
};
template <typename H, unsigned id, typename B>
const LeafState<H, id, B> LeafState<H, id, B>::obj;
// Transition Object
template <typename C, typename S, typename T>
// Current, Source, Target
struct Tran {
typedef typename C::Host Host;
typedef typename C::Base CurrentBase;
typedef typename S::Base SourceBase;
typedef typename T::Base TargetBase;
enum { // work out when to terminate template recursion
eTB_CB = IsDerivedFrom<TargetBase, CurrentBase>::Res,
eS_CB = IsDerivedFrom<S, CurrentBase>::Res,
eS_C = IsDerivedFrom<S, C>::Res,
eC_S = IsDerivedFrom<C, S>::Res,
exitStop = eTB_CB && eS_C,
entryStop = eS_C || eS_CB && !eC_S
};
// We use overloading to stop recursion.
// The more natural template specialization
// method would require to specialize the inner
// template without specializing the outer one,
// which is forbidden.
static void exitActions(Host&, Bool<true>) {}
static void exitActions(Host&h, Bool<false>) {
C::exit(h);
Tran<CurrentBase, S, T>::exitActions(h, Bool<exitStop>());
}
static void entryActions(Host&, Bool<true>) {}
static void entryActions(Host& h, Bool<false>) {
Tran<CurrentBase, S, T>::entryActions(h, Bool<entryStop>());
C::entry(h);
}
Tran(Host & h) : Host_(h) {
exitActions(Host_, Bool<false>());
}
~Tran() {
Tran<T, S, T>::entryActions(Host_, Bool<false>());
T::init(Host_);
}
Host& Host_;
};
// Initializer for Compound States
template <typename T>
struct Init {
typedef typename T::Host Host;
Init(Host& h) : Host_(h) {}
~Init() {
T::entry(Host_);
T::init(Host_);
}
Host& Host_;
};
#endif // HSM_HPP
テストコードが続きます。
#include <cstdio>
#include "hsm.hpp"
#include "hsmtest.hpp"
/* Implements the following state machine from Miro Samek's
* Practical Statecharts in C/C++
*
* |-init-----------------------------------------------------|
* | s0 |
* |----------------------------------------------------------|
* | |
* | |-init-----------| |-------------------------| |
* | | s1 |---c--->| s2 | |
* | |----------------|<--c----|-------------------------| |
* | | | | | |
* |<-d-| |-init-------| | | |-init----------------| | |
* | | | s11 |<----f----| | s21 | | |
* | /--| |------------| | | |---------------------| | |
* | a | | | | | | | | |
* | \->| | |------g--------->|-init------| | | |
* | | |____________| | | |-b->| s211 |---g--->|
* | |----b---^ |------f------->| | | | |
* | |________________| | |<-d-|___________|<--e----|
* | | |_____________________| | |
* | |_________________________| |
* |__________________________________________________________|
*/
class TestHSM;
typedef CompState<TestHSM,0> Top;
typedef CompState<TestHSM,1,Top> S0;
typedef CompState<TestHSM,2,S0> S1;
typedef LeafState<TestHSM,3,S1> S11;
typedef CompState<TestHSM,4,S0> S2;
typedef CompState<TestHSM,5,S2> S21;
typedef LeafState<TestHSM,6,S21> S211;
enum Signal { A_SIG, B_SIG, C_SIG, D_SIG, E_SIG, F_SIG, G_SIG, H_SIG };
class TestHSM {
public:
TestHSM() { Top::init(*this); }
~TestHSM() {}
void next(const TopState<TestHSM>& state) {
state_ = &state;
}
Signal getSig() const { return sig_; }
void dispatch(Signal sig) {
sig_ = sig;
state_->handler(*this);
}
void foo(int i) {
foo_ = i;
}
int foo() const {
return foo_;
}
private:
const TopState<TestHSM>* state_;
Signal sig_;
int foo_;
};
bool testDispatch(char c) {
static TestHSM test;
if (c<'a' || 'h'<c) {
return false;
}
printf("Signal<-%c", c);
test.dispatch((Signal)(c-'a'));
printf("\n");
return true;
}
int main(int, char**) {
testDispatch('a');
testDispatch('e');
testDispatch('e');
testDispatch('a');
testDispatch('h');
testDispatch('h');
return 0;
}
#define HSMHANDLER(State) \
template<> template<typename X> inline void State::handle(TestHSM& h, const X& x) const
HSMHANDLER(S0) {
switch (h.getSig()) {
case E_SIG: { Tran<X, This, S211> t(h);
printf("s0-E;");
return; }
default:
break;
}
return Base::handle(h, x);
}
HSMHANDLER(S1) {
switch (h.getSig()) {
case A_SIG: { Tran<X, This, S1> t(h);
printf("s1-A;"); return; }
case B_SIG: { Tran<X, This, S11> t(h);
printf("s1-B;"); return; }
case C_SIG: { Tran<X, This, S2> t(h);
printf("s1-C;"); return; }
case D_SIG: { Tran<X, This, S0> t(h);
printf("s1-D;"); return; }
case F_SIG: { Tran<X, This, S211> t(h);
printf("s1-F;"); return; }
default: break;
}
return Base::handle(h, x);
}
HSMHANDLER(S11) {
switch (h.getSig()) {
case G_SIG: { Tran<X, This, S211> t(h);
printf("s11-G;"); return; }
case H_SIG: if (h.foo()) {
printf("s11-H");
h.foo(0); return;
} break;
default: break;
}
return Base::handle(h, x);
}
HSMHANDLER(S2) {
switch (h.getSig()) {
case C_SIG: { Tran<X, This, S1> t(h);
printf("s2-C"); return; }
case F_SIG: { Tran<X, This, S11> t(h);
printf("s2-F"); return; }
default: break;
}
return Base::handle(h, x);
}
HSMHANDLER(S21) {
switch (h.getSig()) {
case B_SIG: { Tran<X, This, S211> t(h);
printf("s21-B;"); return; }
case H_SIG: if (!h.foo()) {
Tran<X, This, S21> t(h);
printf("s21-H;"); h.foo(1);
return;
} break;
default: break;
}
return Base::handle(h, x);
}
HSMHANDLER(S211) {
switch (h.getSig()) {
case D_SIG: { Tran<X, This, S21> t(h);
printf("s211-D;"); return; }
case G_SIG: { Tran<X, This, S0> t(h);
printf("s211-G;"); return; }
}
return Base::handle(h, x);
}
#define HSMENTRY(State) \
template<> inline void State::entry(TestHSM&) { \
printf(#State "-ENTRY;"); \
}
HSMENTRY(S0)
HSMENTRY(S1)
HSMENTRY(S11)
HSMENTRY(S2)
HSMENTRY(S21)
HSMENTRY(S211)
#define HSMEXIT(State) \
template<> inline void State::exit(TestHSM&) { \
printf(#State "-EXIT;"); \
}
HSMEXIT(S0)
HSMEXIT(S1)
HSMEXIT(S11)
HSMEXIT(S2)
HSMEXIT(S21)
HSMEXIT(S211)
#define HSMINIT(State, InitState) \
template<> inline void State::init(TestHSM& h) { \
Init<InitState> i(h); \
printf(#State "-INIT;"); \
}
HSMINIT(Top, S0)
HSMINIT(S0, S1)
HSMINIT(S1, S11)
HSMINIT(S2, S21)
HSMINIT(S21, S211)
最も単純なケース
enum event_type { ET_THIS, ET_THAT };
union event_parm { uint8_t this; uint16_t that; }
static void handle_event(enum event_type event, union event_parm parm)
{
static enum { THIS, THAT } state;
switch (state)
{
case THIS:
switch (event)
{
case ET_THIS:
// Handle event.
break;
default:
// Unhandled events in this state.
break;
}
break;
case THAT:
// Handle state.
break;
}
}
ポイント:状態は、コンパイル単位だけでなくevent_handlerに対してもプライベートです。特別な場合は、必要と思われる構成を使用して、メインスイッチとは別に処理できます。
より複雑なケース
スイッチが数画面いっぱいになると、スイッチを各状態を処理する関数に分割し、状態テーブルを使用して関数を直接検索します。状態はまだイベントハンドラーに対してプライベートです。状態ハンドラー関数は次の状態を返します。必要に応じて、一部のイベントはメインイベントハンドラーで特別な処理を受けることができます。私は、状態の開始と終了、およびおそらく状態マシンの起動のために擬似イベントをスローするのが好きです:
enum state_type { THIS, THAT, FOO, NA };
enum event_type { ET_START, ET_ENTER, ET_EXIT, ET_THIS, ET_THAT, ET_WHATEVER, ET_TIMEOUT };
union event_parm { uint8_t this; uint16_t that; };
static void handle_event(enum event_type event, union event_parm parm)
{
static enum state_type state;
static void (* const state_handler[])(enum event_type event, union event_parm parm) = { handle_this, handle_that };
enum state_type next_state = state_handler[state](event, parm);
if (NA != next_state && state != next_state)
{
(void)state_handler[state](ET_EXIT, 0);
state = next_state;
(void)state_handler[state](ET_ENTER, 0);
}
}
特に関数ポインタの配列に関して、構文を打ち込んだかどうかはわかりません。私はこれをコンパイラーで実行していません。確認すると、疑似イベント(state_handler()の呼び出し前の(void)括弧)を処理するときに、次の状態を明示的に破棄するのを忘れたことに気付きました。これは、コンパイラが省略を黙って受け入れたとしても、私がしたいことです。コードの読者に「はい、実際に戻り値を使用せずに関数を呼び出すつもりだった」ことを伝え、静的解析ツールがそれについて警告しないようにします。私は他の誰かがこれをしているのを見たことを覚えていないので、それは特異かもしれません。
ポイント:ステートハンドラー関数はステートの開始および終了時に発生する擬似イベントを楽しむことができるため、ほんの少しの複雑さ(次のステートが現在のステートと異なるかどうかを確認する)を追加すると、他の場所での重複コードを回避できます。状態ハンドラーの結果はこれらのイベントの後に破棄されるため、擬似イベントを処理するときに状態を変更することはできません。もちろん、振る舞いを変更することもできます。
状態ハンドラーは次のようになります。
static enum state_type handle_this(enum event_type event, union event_parm parm)
{
enum state_type next_state = NA;
switch (event)
{
case ET_ENTER:
// Start a timer to do whatever.
// Do other stuff necessary when entering this state.
break;
case ET_WHATEVER:
// Switch state.
next_state = THAT;
break;
case ET_TIMEOUT:
// Switch state.
next_state = FOO;
break;
case ET_EXIT:
// Stop the timer.
// Generally clean up this state.
break;
}
return next_state;
}
より複雑
コンパイル単位が大きくなりすぎた場合(つまり、1000行程度)、各状態ハンドラーを別々のファイルに入れます。各状態ハンドラーがいくつかの画面よりも長くなった場合、状態スイッチが分割された方法と同様に、各イベントを個別の関数に分割します。これは、状態とは別に、または共通のテーブルを使用するか、さまざまなスキームを組み合わせることにより、さまざまな方法で実行できます。それらのいくつかは、他の人によってここでカバーされています。速度が必要な場合は、テーブルを並べ替えてバイナリ検索を使用します。
汎用プログラミング
プリプロセッサで、テーブルの並べ替えや、記述からの状態マシンの生成などの問題を処理して、「プログラムに関するプログラムを作成」できるようにする必要があります。これがBoostの人々がC++テンプレートを活用している理由だと思いますが、構文は不可解です。
2次元テーブル
私は過去に状態/イベントテーブルを使用しましたが、最も単純なケースではそれらが必要だとは思わず、switchステートメントが1画面いっぱいになったとしても、明快さと読みやすさを好みます。より複雑なケースでは、他の人が指摘したように、テーブルはすぐに手に負えなくなります。ここで紹介するイディオムを使用すると、メモリを消費するテーブルを(プログラムメモリであっても)維持しなくても、気になるときに多数のイベントと状態を追加できます。
免責事項
特別なニーズにより、これらのイディオムはあまり役に立たない場合がありますが、私はそれらが非常に明確で保守可能であることがわかりました。
ステートマシン(少なくともプログラム制御用)で私が気に入っている手法は、関数ポインターを使用することです。各状態は異なる関数で表されます。関数は入力シンボルを取り、次の状態の関数ポインターを返します。中央ディスパッチループモニターは次の入力を受け取り、それを現在の状態に送り、結果を処理します。
Cにはそれ自体を返す関数ポインターのタイプを示す方法がないため、状態の関数はvoid*
を返します。ただし、次のようなことができます。
typedef void* (*state_handler)(input_symbol_t);
void dispatch_fsm()
{
state_handler current = initial_handler;
/* Let's assume returning null indicates end-of-machine */
while (current) {
current = current(get_input);
}
}
その後、個々の状態関数は、適切な値を処理して返すために入力を切り替えることができます。
もう1つの興味深いオープンソースツールは、 statecharts.orgのYakindu Statechart Tools です。 Harelステートチャートを利用して、階層的および並列的な状態を提供し、CおよびC++(およびJava)コードを生成します。ライブラリは使用しませんが、「プレーンコード」アプローチに従います。コードは基本的にスイッチケース構造を適用します。コードジェネレーターもカスタマイズできます。さらに、このツールは他の多くの機能を提供します。
私はpaxdiableの答えが本当に好きで、ガード変数やステートマシン固有のデータなど、アプリケーションに欠けている機能をすべて実装することにしました。
このサイトに実装をアップロードして、コミュニティと共有しました。 IAR Embedded Workbench for ARMを使用してテストされています。
非常にテストされていませんが、コーディングするのが楽しくなりました。現在は元の回答よりも洗練されたバージョンです。最新バージョンは Mercurial.intuxication.org にあります。
sm.h
#ifndef SM_ARGS
#error "SM_ARGS undefined: " \
"use '#define SM_ARGS (void)' to get an empty argument list"
#endif
#ifndef SM_STATES
#error "SM_STATES undefined: " \
"you must provide a list of comma-separated states"
#endif
typedef void (*sm_state) SM_ARGS;
static const sm_state SM_STATES;
#define sm_transit(STATE) ((sm_state (*) SM_ARGS)STATE)
#define sm_def(NAME) \
static sm_state NAME ## _fn SM_ARGS; \
static const sm_state NAME = (sm_state)NAME ## _fn; \
static sm_state NAME ## _fn SM_ARGS
example.c
#include <stdio.h>
#define SM_ARGS (int i)
#define SM_STATES EVEN, ODD
#include "sm.h"
sm_def(EVEN)
{
printf("even %i\n", i);
return ODD;
}
sm_def(ODD)
{
printf("odd %i\n", i);
return EVEN;
}
int main(void)
{
int i = 0;
sm_state state = EVEN;
for(; i < 10; ++i)
state = sm_transit(state)(i);
return 0;
}
Alrght、私のものは他の人とは少し違うと思う。他の回答で見たよりもコードとデータの分離が少し多くなっています。これを書くために理論を本当に読み上げました。これは完全な正規言語を実装しています(残念ながら正規表現はありません)。ウルマン、ミンスキー、チョムスキー。私はそれをすべて理解したとは言えませんが、私は古いマスターからできるだけ直接引き出しました:彼らの言葉を通して。
「はい」状態または「いいえ」状態への遷移を決定する述語への関数ポインターを使用します。これにより、アセンブリ言語に似た方法でプログラムする通常言語の有限状態アクセプターの作成が容易になります。私の愚かな名前の選択に先送りされないでください。 'czek' == 'check'。 'grok' == [ハッカー辞書で調べてください]。
そのため、各反復で、czekは現在の文字を引数として述語関数を呼び出します。述部がtrueを返す場合、文字は消費され(ポインターが進み)、「y」遷移に従って次の状態を選択します。述語がfalseを返す場合、文字は消費されず、「n」遷移に従います。したがって、すべての命令は双方向ブランチです!私は当時メルの物語を読んでいたに違いありません。
このコードは 私のポストスクリプトインタープリター から直接来ており、comp.lang.cの仲間からの多くの指導を受けて現在の形式に進化しました。 postscriptには基本的に構文がないため(バランスのとれた括弧のみが必要)、このような通常言語アクセプターはパーサーとしても機能します。
/* currentstr is set to the start of string by czek
and used by setrad (called by israd) to set currentrad
which is used by israddig to determine if the character
in question is valid for the specified radix
--
a little semantic checking in the syntax!
*/
char *currentstr;
int currentrad;
void setrad(void) {
char *end;
currentrad = strtol(currentstr, &end, 10);
if (*end != '#' /* just a sanity check,
the automaton should already have determined this */
|| currentrad > 36
|| currentrad < 2)
fatal("bad radix"); /* should probably be a simple syntaxerror */
}
/*
character classes
used as tests by automatons under control of czek
*/
char *alpha = "0123456789" "ABCDE" "FGHIJ" "KLMNO" "PQRST" "UVWXYZ";
#define EQ(a,b) a==b
#define WITHIN(a,b) strchr(a,b)!=NULL
int israd (int c) {
if (EQ('#',c)) { setrad(); return true; }
return false;
}
int israddig(int c) {
return strchrnul(alpha,toupper(c))-alpha <= currentrad;
}
int isdot (int c) {return EQ('.',c);}
int ise (int c) {return WITHIN("eE",c);}
int issign (int c) {return WITHIN("+-",c);}
int isdel (int c) {return WITHIN("()<>[]{}/%",c);}
int isreg (int c) {return c!=EOF && !isspace(c) && !isdel(c);}
#undef WITHIN
#undef EQ
/*
the automaton type
*/
typedef struct { int (*pred)(int); int y, n; } test;
/*
automaton to match a simple decimal number
*/
/* /^[+-]?[0-9]+$/ */
test fsm_dec[] = {
/* 0*/ { issign, 1, 1 },
/* 1*/ { isdigit, 2, -1 },
/* 2*/ { isdigit, 2, -1 },
};
int acc_dec(int i) { return i==2; }
/*
automaton to match a radix number
*/
/* /^[0-9]+[#][a-Z0-9]+$/ */
test fsm_rad[] = {
/* 0*/ { isdigit, 1, -1 },
/* 1*/ { isdigit, 1, 2 },
/* 2*/ { israd, 3, -1 },
/* 3*/ { israddig, 4, -1 },
/* 4*/ { israddig, 4, -1 },
};
int acc_rad(int i) { return i==4; }
/*
automaton to match a real number
*/
/* /^[+-]?(d+(.d*)?)|(d*.d+)([eE][+-]?d+)?$/ */
/* represents the merge of these (simpler) expressions
[+-]?[0-9]+\.[0-9]*([eE][+-]?[0-9]+)?
[+-]?[0-9]*\.[0-9]+([eE][+-]?[0-9]+)?
The complexity comes from ensuring at least one
digit in the integer or the fraction with optional
sign and optional optionally-signed exponent.
So passing isdot in state 3 means at least one integer digit has been found
but passing isdot in state 4 means we must find at least one fraction digit
via state 5 or the whole thing is a bust.
*/
test fsm_real[] = {
/* 0*/ { issign, 1, 1 },
/* 1*/ { isdigit, 2, 4 },
/* 2*/ { isdigit, 2, 3 },
/* 3*/ { isdot, 6, 7 },
/* 4*/ { isdot, 5, -1 },
/* 5*/ { isdigit, 6, -1 },
/* 6*/ { isdigit, 6, 7 },
/* 7*/ { ise, 8, -1 },
/* 8*/ { issign, 9, 9 },
/* 9*/ { isdigit, 10, -1 },
/*10*/ { isdigit, 10, -1 },
};
int acc_real(int i) {
switch(i) {
case 2: /* integer */
case 6: /* real */
case 10: /* real with exponent */
return true;
}
return false;
}
/*
Helper function for grok.
Execute automaton against the buffer,
applying test to each character:
on success, consume character and follow 'y' transition.
on failure, do not consume but follow 'n' transition.
Call yes function to determine if the ending state
is considered an acceptable final state.
A transition to -1 represents rejection by the automaton
*/
int czek (char *s, test *fsm, int (*yes)(int)) {
int sta = 0;
currentstr = s;
while (sta!=-1 && *s) {
if (fsm[sta].pred((int)*s)) {
sta=fsm[sta].y;
s++;
} else {
sta=fsm[sta].n;
}
}
return yes(sta);
}
/*
Helper function for toke.
Interpret the contents of the buffer,
trying automatons to match number formats;
and falling through to a switch for special characters.
Any token consisting of all regular characters
that cannot be interpreted as a number is an executable name
*/
object grok (state *st, char *s, int ns,
object *src,
int (*next)(state *,object *),
void (*back)(state *,int, object *)) {
if (czek(s, fsm_dec, acc_dec)) {
long num;
num = strtol(s,NULL,10);
if ((num==LONG_MAX || num==LONG_MIN) && errno==ERANGE) {
error(st,limitcheck);
/* } else if (num > INT_MAX || num < INT_MIN) { */
/* error(limitcheck, OP_token); */
} else {
return consint(num);
}
}
else if (czek(s, fsm_rad, acc_rad)) {
long ra,num;
ra = (int)strtol(s,NULL,10);
if (ra > 36 || ra < 2) {
error(st,limitcheck);
}
num = strtol(strchr(s,'#')+1, NULL, (int)ra);
if ((num==LONG_MAX || num==LONG_MIN) && errno==ERANGE) {
error(st,limitcheck);
/* } else if (num > INT_MAX || num < INT_MAX) { */
/* error(limitcheck, OP_token); */
} else {
return consint(num);
}
}
else if (czek(s, fsm_real, acc_real)) {
double num;
num = strtod(s,NULL);
if ((num==HUGE_VAL || num==-HUGE_VAL) && errno==ERANGE) {
error(st,limitcheck);
} else {
return consreal(num);
}
}
else switch(*s) {
case '(': {
int c, defer=1;
char *sp = s;
while (defer && (c=next(st,src)) != EOF ) {
switch(c) {
case '(': defer++; break;
case ')': defer--;
if (!defer) goto endstring;
break;
case '\\': c=next(st,src);
switch(c) {
case '\n': continue;
case 'a': c = '\a'; break;
case 'b': c = '\b'; break;
case 'f': c = '\f'; break;
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'v': c = '\v'; break;
case '\'': case '\"':
case '(': case ')':
default: break;
}
}
if (sp-s>ns) error(st,limitcheck);
else *sp++ = c;
}
endstring: *sp=0;
return cvlit(consstring(st,s,sp-s));
}
case '<': {
int c;
char d, *x = "0123456789abcdef", *sp = s;
while (c=next(st,src), c!='>' && c!=EOF) {
if (isspace(c)) continue;
if (isxdigit(c)) c = strchr(x,tolower(c)) - x;
else error(st,syntaxerror);
d = (char)c << 4;
while (isspace(c=next(st,src))) /*loop*/;
if (isxdigit(c)) c = strchr(x,tolower(c)) - x;
else error(st,syntaxerror);
d |= (char)c;
if (sp-s>ns) error(st,limitcheck);
*sp++ = d;
}
*sp = 0;
return cvlit(consstring(st,s,sp-s));
}
case '{': {
object *a;
size_t na = 100;
size_t i;
object proc;
object fin;
fin = consname(st,"}");
(a = malloc(na * sizeof(object))) || (fatal("failure to malloc"),0);
for (i=0 ; objcmp(st,a[i]=toke(st,src,next,back),fin) != 0; i++) {
if (i == na-1)
(a = realloc(a, (na+=100) * sizeof(object))) || (fatal("failure to malloc"),0);
}
proc = consarray(st,i);
{ size_t j;
for (j=0; j<i; j++) {
a_put(st, proc, j, a[j]);
}
}
free(a);
return proc;
}
case '/': {
s[1] = (char)next(st,src);
puff(st, s+2, ns-2, src, next, back);
if (s[1] == '/') {
Push(consname(st,s+2));
opexec(st, op_cuts.load);
return pop();
}
return cvlit(consname(st,s+1));
}
default: return consname(st,s);
}
return null; /* should be unreachable */
}
/*
Helper function for toke.
Read into buffer any regular characters.
If we read one too many characters, put it back
unless it's whitespace.
*/
int puff (state *st, char *buf, int nbuf,
object *src,
int (*next)(state *,object *),
void (*back)(state *,int, object *)) {
int c;
char *s = buf;
while (isreg(c=next(st,src))) {
if (s-buf >= nbuf-1) return false;
*s++ = c;
}
*s = 0;
if (!isspace(c) && c != EOF) back(st,c,src); /* eat interstice */
return true;
}
/*
Helper function for Stoken Ftoken.
Read a token from src using next and back.
Loop until having read a bona-fide non-whitespace non-comment character.
Call puff to read into buffer up to next delimiter or space.
Call grok to figure out what it is.
*/
#define NBUF MAXLINE
object toke (state *st, object *src,
int (*next)(state *, object *),
void (*back)(state *, int, object *)) {
char buf[NBUF] = "", *s=buf;
int c,sta = 1;
object o;
do {
c=next(st,src);
//if (c==EOF) return null;
if (c=='%') {
if (DUMPCOMMENTS) fputc(c, stdout);
do {
c=next(st,src);
if (DUMPCOMMENTS) fputc(c, stdout);
} while (c!='\n' && c!='\f' && c!=EOF);
}
} while (c!=EOF && isspace(c));
if (c==EOF) return null;
*s++ = c;
*s = 0;
if (!isdel(c)) sta=puff(st, s,NBUF-1,src,next,back);
if (sta) {
o=grok(st,buf,NBUF-1,src,next,back);
return o;
} else {
return null;
}
}
boost.orgには、2つの異なる状態図の実装が付属しています。
いつものように、ブーストはテンプレートの地獄にあなたをビームします。
最初のライブラリは、よりパフォーマンスが重要な状態マシン用です。 2番目のライブラリは、UMLステートチャートからコードへの直接の移行パスを提供します。
ここに SOの2つの比較を求める質問 があり、両方の著者が応答します。
これまでに(いつものように)遅くなりましたが、今日までの答えをスキャンすると、重要なものが欠けていると思います。
私は自分のプロジェクトで、notがすべての有効な状態/イベントの組み合わせに対して機能を持つことが非常に役立つことを発見しました。状態/イベントの2Dテーブルを効果的に作成するというアイデアが好きです。しかし、テーブル要素は単純な関数ポインター以上のものであることが好きです。その代わりに、私は自分のデザインを整理しようと心がけています。そうすれば、状態/イベントテーブルの各交点にこれらの単純なアトミック要素をリストできます。アイデアは、do n't N個の2乗(通常非常に単純な)関数の質量を定義する必要があるということです。なぜエラーが発生しやすく、時間がかかり、書きにくく、読みにくいのですか?
また、オプションの新しい状態、およびテーブル内の各セルのオプションの関数ポインターも含めます。関数ポインターは、アトミックアクションのリストだけを起動したくない例外的な場合に使用します。
テーブルを編集するだけで、新しいコードを記述することなく、多くの異なる機能を表現できるときに、あなたが正しいことをしていることを知っています。
この 一連のArs OpenForumの投稿 制御ロジックのやや複雑な部分には、Cのステートマシンとして非常にわかりやすい実装が含まれています。
これをどこかで見た
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
C++を使用してOOコードを使用できることを示唆しているので、「GoF」状態パターン(GoF = Gang of Four、デザインパターンをもたらしたデザインパターンの本を書いた人たち)脚光)。
特に複雑ではなく、広く使用され、議論されているので、例や説明をオンラインで簡単に見ることができます。
また、後日あなたのコードを保守する他の誰にも認識される可能性が高いでしょう。
効率が心配な場合は、多くの要因がパフォーマンスに影響し、常にOOが悪いとは限らないため、非OOアプローチがより効率的であることを実際にベンチマークする価値があります。機能的なコードが良い。同様に、メモリ使用量が制約である場合は、状態パターンを使用する場合、これが実際に特定のアプリケーションの問題になるかどうかを確認するために、いくつかのテストまたは計算を行う価値があります。
以下は、クレイグが示唆するように、「Gof」状態パターンへのリンクです。
あなたの質問は非常に一般的です、
役立つ参考資料が2つありますが、
この記事では、組み込みシステム用のステートマシンを実装する簡単な方法について説明します。この記事の目的上、ステートマシンは、少数の状態のいずれかになりうるアルゴリズムとして定義されます。状態とは、入力と出力の関係、および入力と次の状態の関係を規定する条件です。
。 Mealyマシンは、出力が状態のみの関数であるムーアマシンとは対照的に、出力が現在の状態と入力の両方の関数である状態マシンです。
この記事での私の関心は、ステートマシンの基礎と、CまたはC++でステートマシンをコーディングするための簡単なプログラミングガイドラインです。これらの単純な手法がより一般的になり、あなた(および他の人)がソースコードからステートマシン構造をすぐに見ることができることを願っています。
これは多くの答えがある古い投稿ですが、Cの有限状態マシンに独自のアプローチを追加すると思いました。Pythonスクリプトを作成して、任意の数の状態のスケルトンCコードを生成しました。このスクリプトは、GituHubの FsmTemplateC に記載されています。
この例は、私が読んだ他のアプローチに基づいています。 gotoまたはswitchステートメントは使用しませんが、代わりにポインターマトリックス(ルックアップテーブル)に遷移関数があります。このコードは、大きな複数行イニシャライザーマクロとC99機能(指定されたイニシャライザーと複合リテラル)に依存しているため、これらのものが気に入らない場合は、このアプローチが気に入らないかもしれません。
ターンスタイルの例 のPythonスクリプトは、 FsmTemplateC を使用してスケルトンCコードを生成します。
# dict parameter for generating FSM
fsm_param = {
# main FSM struct type string
'type': 'FsmTurnstile',
# struct type and name for passing data to state machine functions
# by pointer (these custom names are optional)
'fopts': {
'type': 'FsmTurnstileFopts',
'name': 'fopts'
},
# list of states
'states': ['locked', 'unlocked'],
# list of inputs (can be any length > 0)
'inputs': ['coin', 'Push'],
# map inputs to commands (next desired state) using a transition table
# index of array corresponds to 'inputs' array
# for this example, index 0 is 'coin', index 1 is 'Push'
'transitiontable': {
# current state | 'coin' | 'Push' |
'locked': ['unlocked', ''],
'unlocked': [ '', 'locked']
}
}
# folder to contain generated code
folder = 'turnstile_example'
# function prefix
prefix = 'fsm_turnstile'
# generate FSM code
code = fsm.Fsm(fsm_param).genccode(folder, prefix)
生成された出力ヘッダーにはtypedefが含まれます。
/* function options (EDIT) */
typedef struct FsmTurnstileFopts {
/* define your options struct here */
} FsmTurnstileFopts;
/* transition check */
typedef enum eFsmTurnstileCheck {
EFSM_TURNSTILE_TR_RETREAT,
EFSM_TURNSTILE_TR_ADVANCE,
EFSM_TURNSTILE_TR_CONTINUE,
EFSM_TURNSTILE_TR_BADINPUT
} eFsmTurnstileCheck;
/* states (enum) */
typedef enum eFsmTurnstileState {
EFSM_TURNSTILE_ST_LOCKED,
EFSM_TURNSTILE_ST_UNLOCKED,
EFSM_TURNSTILE_NUM_STATES
} eFsmTurnstileState;
/* inputs (enum) */
typedef enum eFsmTurnstileInput {
EFSM_TURNSTILE_IN_COIN,
EFSM_TURNSTILE_IN_Push,
EFSM_TURNSTILE_NUM_INPUTS,
EFSM_TURNSTILE_NOINPUT
} eFsmTurnstileInput;
/* finite state machine struct */
typedef struct FsmTurnstile {
eFsmTurnstileInput input;
eFsmTurnstileCheck check;
eFsmTurnstileState cur;
eFsmTurnstileState cmd;
eFsmTurnstileState **transition_table;
void (***state_transitions)(struct FsmTurnstile *, FsmTurnstileFopts *);
void (*run)(struct FsmTurnstile *, FsmTurnstileFopts *, const eFsmTurnstileInput);
} FsmTurnstile;
/* transition functions */
typedef void (*pFsmTurnstileStateTransitions)(struct FsmTurnstile *, FsmTurnstileFopts *);
eFsmTurnstileCheck
は、遷移がEFSM_TURNSTILE_TR_RETREAT
でブロックされたか、EFSM_TURNSTILE_TR_ADVANCE
で進行できるか、または関数呼び出しの前にEFSM_TURNSTILE_TR_CONTINUE
で遷移がなかったかを判断するために使用されます。eFsmTurnstileState
は、単に状態のリストです。eFsmTurnstileInput
は、単に入力のリストです。FsmTurnstile
構造体は、状態チェックの中心であり、遷移チェック、関数ルックアップテーブル、現在の状態、コマンドされた状態、およびマシンを実行するプライマリ関数のエイリアスを持ちます。FsmTurnstile
内のすべての関数ポインター(エイリアス)は、構造体からのみ呼び出す必要があり、オブジェクト指向の永続的な状態を維持するために、最初の入力を自身へのポインターとして持つ必要があります。ヘッダー内の関数宣言の場合:
/* fsm declarations */
void fsm_turnstile_locked_locked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_locked_unlocked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_unlocked_locked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_unlocked_unlocked (FsmTurnstile *fsm, FsmTurnstileFopts *fopts);
void fsm_turnstile_run (FsmTurnstile *fsm, FsmTurnstileFopts *fopts, const eFsmTurnstileInput input);
関数名の形式は{prefix}_{from}_{to}
です。{from}
は前の(現在の)状態で、{to}
は次の状態です。遷移テーブルが特定の遷移を許可しない場合、関数ポインターの代わりにNULLポインターが設定されることに注意してください。最後に、魔法はマクロで起こります。ここで、遷移テーブル(状態列挙のマトリックス)と状態遷移関数ルックアップテーブル(関数ポインターのマトリックス)を作成します。
/* creation macro */
#define FSM_TURNSTILE_CREATE() \
{ \
.input = EFSM_TURNSTILE_NOINPUT, \
.check = EFSM_TURNSTILE_TR_CONTINUE, \
.cur = EFSM_TURNSTILE_ST_LOCKED, \
.cmd = EFSM_TURNSTILE_ST_LOCKED, \
.transition_table = (eFsmTurnstileState * [EFSM_TURNSTILE_NUM_STATES]) { \
(eFsmTurnstileState [EFSM_TURNSTILE_NUM_INPUTS]) { \
EFSM_TURNSTILE_ST_UNLOCKED, \
EFSM_TURNSTILE_ST_LOCKED \
}, \
(eFsmTurnstileState [EFSM_TURNSTILE_NUM_INPUTS]) { \
EFSM_TURNSTILE_ST_UNLOCKED, \
EFSM_TURNSTILE_ST_LOCKED \
} \
}, \
.state_transitions = (pFsmTurnstileStateTransitions * [EFSM_TURNSTILE_NUM_STATES]) { \
(pFsmTurnstileStateTransitions [EFSM_TURNSTILE_NUM_STATES]) { \
fsm_turnstile_locked_locked, \
fsm_turnstile_locked_unlocked \
}, \
(pFsmTurnstileStateTransitions [EFSM_TURNSTILE_NUM_STATES]) { \
fsm_turnstile_unlocked_locked, \
fsm_turnstile_unlocked_unlocked \
} \
}, \
.run = fsm_turnstile_run \
}
FSMを作成するときは、マクロFSM_EXAMPLE_CREATE()
を使用する必要があります。
これで、ソースコードでは、上記で宣言されたすべての状態遷移関数に値を設定する必要があります。 FsmTurnstileFopts
構造体を使用して、状態マシンとの間でデータをやり取りできます。すべての遷移は、fsm->check
をEFSM_EXAMPLE_TR_RETREAT
に設定して、遷移をブロックするか、EFSM_EXAMPLE_TR_ADVANCE
をコマンド状態に遷移できるようにする必要があります。実際の例は、(FsmTemplateC)[ https://github.com/ChisholmKyle/FsmTemplateC] にあります。
コードでの実際の使用法は非常に簡単です。
/* create fsm */
FsmTurnstile fsm = FSM_TURNSTILE_CREATE();
/* create fopts */
FsmTurnstileFopts fopts = {
.msg = ""
};
/* initialize input */
eFsmTurnstileInput input = EFSM_TURNSTILE_NOINPUT;
/* main loop */
for (;;) {
/* wait for timer signal, inputs, interrupts, whatever */
/* optionally set the input (my_input = EFSM_TURNSTILE_IN_Push for example) */
/* run state machine */
my_fsm.run(&my_fsm, &my_fopts, my_input);
}
ヘッダービジネスとすべての機能は、シンプルで高速なインターフェイスを実現するためだけのものです。
JavaおよびPythonプロジェクトで State Machine Compiler を使用して成功しました。
void (* StateController)(void);
void state1(void);
void state2(void);
void main()
{
StateController=&state1;
while(1)
{
(* StateController)();
}
}
void state1(void)
{
//do something in state1
StateController=&state2;
}
void state2(void)
{
//do something in state2
//Keep changing function direction based on state transition
StateController=&state1;
}
オープンソースライブラリ OpenFST を使用できます。
OpenFstは、重み付き有限状態トランスデューサ(FST)を構築、結合、最適化、および検索するためのライブラリです。重み付き有限状態トランスデューサは、各遷移に入力ラベル、出力ラベル、および重みがあるオートマトンです。より馴染みのある有限状態アクセプターは、各遷移の入力ラベルと出力ラベルが等しいトランスデューサーとして表されます。有限状態アクセプターは、文字列のセット(具体的には、通常のセットまたは合理的なセット)を表すために使用されます。有限状態トランスデューサーは、ストリングのペア間のバイナリ関係(具体的には、合理的な変換)を表すために使用されます。重みを使用して、特定の移行にかかるコストを表すことができます。
私は個人的に、ポインター配列と組み合わせて自己参照構造体を使用しています。しばらく前にgithubにチュートリアルをアップロードしました。リンク:
https://github.com/mmelchger/polling_state_machine_c
注:このスレッドは非常に古いことを理解していますが、ステートマシンの設計に関する入力と考えを取得し、Cで可能なステートマシン設計の例を提供できることを願っています。
次に、イベントとしてメッセージキューを使用するLinux用の有限状態マシンの例を示します。イベントはキューに入れられ、順番に処理されます。状態は、各イベントの発生内容に応じて変化します。
これは、次のような状態のデータ接続の例です。
追加した1つの小さな追加機能は、各メッセージ/イベントのタイムスタンプでした。イベントハンドラーは、古すぎる(期限切れの)イベントを無視します。これは、予想外の状態に陥る可能性がある現実の世界で多く発生する可能性があります。
この例はLinuxで実行されます。以下のMakefileを使用してコンパイルし、試してみてください。
state_machine.c
#include <stdio.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h> // sysconf()
#include <errno.h> // errno
#include <string.h> // strerror()
#include <sys/time.h> // gettimeofday()
#include <fcntl.h> // For O_* constants
#include <sys/stat.h> // For mode constants
#include <mqueue.h>
#include <poll.h>
//------------------------------------------------
// States
//------------------------------------------------
typedef enum
{
ST_UNKNOWN = 0,
ST_UNINIT,
ST_INIT,
ST_CONNECTED,
ST_MTU_NEGOTIATED,
ST_AUTHENTICATED,
ST_ERROR,
ST_DONT_CHANGE,
ST_TERM,
} fsmState_t;
//------------------------------------------------
// Events
//------------------------------------------------
typedef enum
{
EV_UNKNOWN = 0,
EV_INIT_SUCCESS,
EV_INIT_FAIL,
EV_MASTER_CMD_MSG,
EV_CONNECT_SUCCESS,
EV_CONNECT_FAIL,
EV_MTU_SUCCESS,
EV_MTU_FAIL,
EV_AUTH_SUCCESS,
EV_AUTH_FAIL,
EV_TX_SUCCESS,
EV_TX_FAIL,
EV_DISCONNECTED,
EV_DISCON_FAILED,
EV_LAST_ENTRY,
} fsmEvName_t;
typedef struct fsmEvent_type
{
fsmEvName_t name;
struct timeval genTime; // Time the event was generated.
// This allows us to see how old the event is.
} fsmEvent_t;
// Finite State Machine Data Members
typedef struct fsmData_type
{
int connectTries;
int MTUtries;
int authTries;
int txTries;
} fsmData_t;
// Each row of the state table
typedef struct stateTable_type {
fsmState_t st; // Current state
fsmEvName_t evName; // Got this event
int (*conditionfn)(void *); // If this condition func returns TRUE
fsmState_t nextState; // Change to this state and
void (*fn)(void *); // Run this function
} stateTable_t;
// Finite State Machine state structure
typedef struct fsm_type
{
const stateTable_t *pStateTable; // Pointer to state table
int numStates; // Number of entries in the table
fsmState_t currentState; // Current state
fsmEvent_t currentEvent; // Current event
fsmData_t *fsmData; // Pointer to the data attributes
mqd_t mqdes; // Message Queue descriptor
mqd_t master_cmd_mqdes; // Master command message queue
} fsm_t;
// Wildcard events and wildcard state
#define EV_ANY -1
#define ST_ANY -1
#define TRUE (1)
#define FALSE (0)
// Maximum priority for message queues (see "man mq_overview")
#define FSM_PRIO (sysconf(_SC_MQ_PRIO_MAX) - 1)
static void addev (fsm_t *fsm, fsmEvName_t ev);
static void doNothing (void *fsm) {addev(fsm, EV_MASTER_CMD_MSG);}
static void doInit (void *fsm) {addev(fsm, EV_INIT_SUCCESS);}
static void doConnect (void *fsm) {addev(fsm, EV_CONNECT_SUCCESS);}
static void doMTU (void *fsm) {addev(fsm, EV_MTU_SUCCESS);}
static void reportFailConnect (void *fsm) {addev(fsm, EV_ANY);}
static void doAuth (void *fsm) {addev(fsm, EV_AUTH_SUCCESS);}
static void reportDisConnect (void *fsm) {addev(fsm, EV_ANY);}
static void doDisconnect (void *fsm) {addev(fsm, EV_ANY);}
static void doTransaction (void *fsm) {addev(fsm, EV_TX_FAIL);}
static void fsmError (void *fsm) {addev(fsm, EV_ANY);}
static int currentlyLessThanMaxConnectTries (void *fsm) {
fsm_t *l = (fsm_t *)fsm;
return (l->fsmData->connectTries < 5 ? TRUE : FALSE);
}
static int isMoreThanMaxConnectTries (void *fsm) {return TRUE;}
static int currentlyLessThanMaxMTUtries (void *fsm) {return TRUE;}
static int isMoreThanMaxMTUtries (void *fsm) {return TRUE;}
static int currentyLessThanMaxAuthTries (void *fsm) {return TRUE;}
static int isMoreThanMaxAuthTries (void *fsm) {return TRUE;}
static int currentlyLessThanMaxTXtries (void *fsm) {return FALSE;}
static int isMoreThanMaxTXtries (void *fsm) {return TRUE;}
static int didNotSelfDisconnect (void *fsm) {return TRUE;}
static int waitForEvent (fsm_t *fsm);
static void runEvent (fsm_t *fsm);
static void runStateMachine(fsm_t *fsm);
static int newEventIsValid(fsmEvent_t *event);
static void getTime(struct timeval *time);
void printState(fsmState_t st);
void printEvent(fsmEvName_t ev);
// Global State Table
const stateTable_t GST[] = {
// Current state Got this event If this condition func returns TRUE Change to this state and Run this function
{ ST_UNINIT, EV_INIT_SUCCESS, NULL, ST_INIT, &doNothing },
{ ST_UNINIT, EV_INIT_FAIL, NULL, ST_UNINIT, &doInit },
{ ST_INIT, EV_MASTER_CMD_MSG, NULL, ST_INIT, &doConnect },
{ ST_INIT, EV_CONNECT_SUCCESS, NULL, ST_CONNECTED, &doMTU },
{ ST_INIT, EV_CONNECT_FAIL, ¤tlyLessThanMaxConnectTries, ST_INIT, &doConnect },
{ ST_INIT, EV_CONNECT_FAIL, &isMoreThanMaxConnectTries, ST_INIT, &reportFailConnect },
{ ST_CONNECTED, EV_MTU_SUCCESS, NULL, ST_MTU_NEGOTIATED, &doAuth },
{ ST_CONNECTED, EV_MTU_FAIL, ¤tlyLessThanMaxMTUtries, ST_CONNECTED, &doMTU },
{ ST_CONNECTED, EV_MTU_FAIL, &isMoreThanMaxMTUtries, ST_CONNECTED, &doDisconnect },
{ ST_CONNECTED, EV_DISCONNECTED, &didNotSelfDisconnect, ST_INIT, &reportDisConnect },
{ ST_MTU_NEGOTIATED, EV_AUTH_SUCCESS, NULL, ST_AUTHENTICATED, &doTransaction },
{ ST_MTU_NEGOTIATED, EV_AUTH_FAIL, ¤tyLessThanMaxAuthTries, ST_MTU_NEGOTIATED, &doAuth },
{ ST_MTU_NEGOTIATED, EV_AUTH_FAIL, &isMoreThanMaxAuthTries, ST_MTU_NEGOTIATED, &doDisconnect },
{ ST_MTU_NEGOTIATED, EV_DISCONNECTED, &didNotSelfDisconnect, ST_INIT, &reportDisConnect },
{ ST_AUTHENTICATED, EV_TX_SUCCESS, NULL, ST_AUTHENTICATED, &doDisconnect },
{ ST_AUTHENTICATED, EV_TX_FAIL, ¤tlyLessThanMaxTXtries, ST_AUTHENTICATED, &doTransaction },
{ ST_AUTHENTICATED, EV_TX_FAIL, &isMoreThanMaxTXtries, ST_AUTHENTICATED, &doDisconnect },
{ ST_AUTHENTICATED, EV_DISCONNECTED, &didNotSelfDisconnect, ST_INIT, &reportDisConnect },
{ ST_ANY, EV_DISCON_FAILED, NULL, ST_DONT_CHANGE, &doDisconnect },
{ ST_ANY, EV_ANY, NULL, ST_UNINIT, &fsmError } // Wildcard state for errors
};
#define GST_COUNT (sizeof(GST)/sizeof(stateTable_t))
int main()
{
int ret = 0;
fsmData_t dataAttr;
dataAttr.connectTries = 0;
dataAttr.MTUtries = 0;
dataAttr.authTries = 0;
dataAttr.txTries = 0;
fsm_t lfsm;
memset(&lfsm, 0, sizeof(fsm_t));
lfsm.pStateTable = GST;
lfsm.numStates = GST_COUNT;
lfsm.currentState = ST_UNINIT;
lfsm.currentEvent.name = EV_ANY;
lfsm.fsmData = &dataAttr;
struct mq_attr attr;
attr.mq_maxmsg = 30;
attr.mq_msgsize = sizeof(fsmEvent_t);
// Dev info
//printf("Size of fsmEvent_t [%ld]\n", sizeof(fsmEvent_t));
ret = mq_unlink("/abcmq");
if (ret == -1) {
fprintf(stderr, "Error on mq_unlink(), errno[%d] strerror[%s]\n",
errno, strerror(errno));
}
lfsm.mqdes = mq_open("/abcmq", O_CREAT | O_RDWR, S_IWUSR | S_IRUSR, &attr);
if (lfsm.mqdes == (mqd_t)-1) {
fprintf(stderr, "Error on mq_open(), errno[%d] strerror[%s]\n",
errno, strerror(errno));
return -1;
}
doInit(&lfsm); // This will generate the first event
runStateMachine(&lfsm);
return 0;
}
static void runStateMachine(fsm_t *fsm)
{
int ret = 0;
if (fsm == NULL) {
fprintf(stderr, "[%s] NULL argument\n", __func__);
return;
}
// Cycle through the state machine
while (fsm->currentState != ST_TERM) {
printf("current state [");
printState(fsm->currentState);
printf("]\n");
ret = waitForEvent(fsm);
if (ret == 0) {
printf("got event [");
printEvent(fsm->currentEvent.name);
printf("]\n");
runEvent(fsm);
}
sleep(2);
}
}
static int waitForEvent(fsm_t *fsm)
{
//const int numFds = 2;
const int numFds = 1;
struct pollfd fds[numFds];
int timeout_msecs = -1; // -1 is forever
int ret = 0;
int i = 0;
ssize_t num = 0;
fsmEvent_t newEv;
if (fsm == NULL) {
fprintf(stderr, "[%s] NULL argument\n", __func__);
return -1;
}
fsm->currentEvent.name = EV_ANY;
fds[0].fd = fsm->mqdes;
fds[0].events = POLLIN;
//fds[1].fd = fsm->master_cmd_mqdes;
//fds[1].events = POLLIN;
ret = poll(fds, numFds, timeout_msecs);
if (ret > 0) {
// An event on one of the fds has occurred
for (i = 0; i < numFds; i++) {
if (fds[i].revents & POLLIN) {
// Data may be read on device number i
num = mq_receive(fds[i].fd, (void *)(&newEv),
sizeof(fsmEvent_t), NULL);
if (num == -1) {
fprintf(stderr, "Error on mq_receive(), errno[%d] "
"strerror[%s]\n", errno, strerror(errno));
return -1;
}
if (newEventIsValid(&newEv)) {
fsm->currentEvent = newEv;
} else {
return -1;
}
}
}
} else {
fprintf(stderr, "Error on poll(), ret[%d] errno[%d] strerror[%s]\n",
ret, errno, strerror(errno));
return -1;
}
return 0;
}
static int newEventIsValid(fsmEvent_t *event)
{
if (event == NULL) {
fprintf(stderr, "[%s] NULL argument\n", __func__);
return FALSE;
}
printf("[%s]\n", __func__);
struct timeval now;
getTime(&now);
if ( (event->name < EV_LAST_ENTRY) &&
((now.tv_sec - event->genTime.tv_sec) < (60*5))
)
{
return TRUE;
} else {
return FALSE;
}
}
//------------------------------------------------
// Performs event handling on the FSM (finite state machine).
// Make sure there is a wildcard state at the end of
// your table, otherwise; the event will be ignored.
//------------------------------------------------
static void runEvent(fsm_t *fsm)
{
int i;
int condRet = 0;
if (fsm == NULL) {
fprintf(stderr, "[%s] NULL argument\n", __func__);
return;
}
printf("[%s]\n", __func__);
// Find a relevant entry for this state and event
for (i = 0; i < fsm->numStates; i++) {
// Look in the table for our current state or ST_ANY
if ( (fsm->pStateTable[i].st == fsm->currentState) ||
(fsm->pStateTable[i].st == ST_ANY)
)
{
// Is this the event we are looking for?
if ( (fsm->pStateTable[i].evName == fsm->currentEvent.name) ||
(fsm->pStateTable[i].evName == EV_ANY)
)
{
if (fsm->pStateTable[i].conditionfn != NULL) {
condRet = fsm->pStateTable[i].conditionfn(fsm->fsmData);
}
// See if there is a condition associated
// or we are not looking for any condition
//
if ( (condRet != 0) || (fsm->pStateTable[i].conditionfn == NULL))
{
// Set the next state (if applicable)
if (fsm->pStateTable[i].nextState != ST_DONT_CHANGE) {
fsm->currentState = fsm->pStateTable[i].nextState;
printf("new state [");
printState(fsm->currentState);
printf("]\n");
}
// Call the state callback function
fsm->pStateTable[i].fn(fsm);
break;
}
}
}
}
}
//------------------------------------------------
// EVENT HANDLERS
//------------------------------------------------
static void getTime(struct timeval *time)
{
if (time == NULL) {
fprintf(stderr, "[%s] NULL argument\n", __func__);
return;
}
printf("[%s]\n", __func__);
int ret = gettimeofday(time, NULL);
if (ret != 0) {
fprintf(stderr, "gettimeofday() failed: errno [%d], strerror [%s]\n",
errno, strerror(errno));
memset(time, 0, sizeof(struct timeval));
}
}
static void addev (fsm_t *fsm, fsmEvName_t ev)
{
int ret = 0;
if (fsm == NULL) {
fprintf(stderr, "[%s] NULL argument\n", __func__);
return;
}
printf("[%s] ev[%d]\n", __func__, ev);
if (ev == EV_ANY) {
// Don't generate a new event, just return...
return;
}
fsmEvent_t newev;
getTime(&(newev.genTime));
newev.name = ev;
ret = mq_send(fsm->mqdes, (void *)(&newev), sizeof(fsmEvent_t), FSM_PRIO);
if (ret == -1) {
fprintf(stderr, "[%s] mq_send() failed: errno [%d], strerror [%s]\n",
__func__, errno, strerror(errno));
}
}
//------------------------------------------------
// end EVENT HANDLERS
//------------------------------------------------
void printState(fsmState_t st)
{
switch(st) {
case ST_UNKNOWN:
printf("ST_UNKNOWN");
break;
case ST_UNINIT:
printf("ST_UNINIT");
break;
case ST_INIT:
printf("ST_INIT");
break;
case ST_CONNECTED:
printf("ST_CONNECTED");
break;
case ST_MTU_NEGOTIATED:
printf("ST_MTU_NEGOTIATED");
break;
case ST_AUTHENTICATED:
printf("ST_AUTHENTICATED");
break;
case ST_ERROR:
printf("ST_ERROR");
break;
case ST_TERM:
printf("ST_TERM");
break;
default:
printf("unknown state");
break;
}
}
void printEvent(fsmEvName_t ev)
{
switch (ev) {
case EV_UNKNOWN:
printf("EV_UNKNOWN");
break;
case EV_INIT_SUCCESS:
printf("EV_INIT_SUCCESS");
break;
case EV_INIT_FAIL:
printf("EV_INIT_FAIL");
break;
case EV_MASTER_CMD_MSG:
printf("EV_MASTER_CMD_MSG");
break;
case EV_CONNECT_SUCCESS:
printf("EV_CONNECT_SUCCESS");
break;
case EV_CONNECT_FAIL:
printf("EV_CONNECT_FAIL");
break;
case EV_MTU_SUCCESS:
printf("EV_MTU_SUCCESS");
break;
case EV_MTU_FAIL:
printf("EV_MTU_FAIL");
break;
case EV_AUTH_SUCCESS:
printf("EV_AUTH_SUCCESS");
break;
case EV_AUTH_FAIL:
printf("EV_AUTH_FAIL");
break;
case EV_TX_SUCCESS:
printf("EV_TX_SUCCESS");
break;
case EV_TX_FAIL:
printf("EV_TX_FAIL");
break;
case EV_DISCONNECTED:
printf("EV_DISCONNECTED");
break;
case EV_LAST_ENTRY:
printf("EV_LAST_ENTRY");
break;
default:
printf("unknown event");
break;
}
}
Makefile
CXX = gcc
COMPFLAGS = -c -Wall -g
state_machine: state_machine.o
$(CXX) -lrt state_machine.o -o state_machine
state_machine.o: state_machine.c
$(CXX) $(COMPFLAGS) state_machine.c
clean:
rm state_machine state_machine.o