CとC ++が混在する小さなプロジェクトを作成しています。私は、ワーカースレッドの1つの中心に、小さな風のステートマシンを1つ構築しています。
SOの達人があなたのステートマシン設計テクニックを共有するかどうか疑問に思っていました。
注:私は主に、試行錯誤した実装技術の後にいます。
更新: SOで収集されたすべての優れた入力に基づいて、私はこのアーキテクチャで解決しました:
CとC ++が混在する小さなプロジェクトを作成しています。私は、ワーカースレッドの1つの中心に、小さな風のステートマシンを1つ構築しています。
SOの達人があなたのステートマシン設計テクニックを共有するかどうか疑問に思っていました。
注:私は主に、試行錯誤した実装技術の後にいます。
更新: 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回限り、プロセス開始時、構成ファイルの読み取り)であり、複数のインスタンスを実行する必要がないためです。 。しかし、複数を実行する必要がある場合は、価値があります。
int (*fn)(void*);
どこでvoid*
あるかを状態関数に定義すると、おそらくよりクリーンになります。次に、状態関数はデータを使用するか、無視します。
他の答えは良いですが、状態マシンが非常に単純なときに使用した非常に「軽量」な実装は次のようになります。
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
ステートメントが効率的であるだけでなく、コードがより簡潔で読みやすくなる数少ない(私は2つしか数えられません)場所の1つです。なぜならgoto
ステートメントはラベルに基づいている、数字の混乱を追跡したり、列挙型を使用したりする代わりに、状態に名前を付けることができます。また、関数ポインターや巨大な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 ++を含む数十の言語のいずれかにコンパイルします。ユーティリティ自体はJavaで記述されており、ビルドの一部として含めることができます。
これを行う理由は、GoF状態パターンまたはその他のアプローチを使用して手動でコーディングするのではなく、ステートマシンがコードとして表現されると、それをサポートするために生成する必要があるボイラープレートの重みで基礎となる構造が消える傾向があるためです。このアプローチを使用すると、懸念事項を非常に分離し、ステートマシンの構造を「可視」に保つことができます。自動生成されたコードは、触れる必要のないモジュールに入ります。そのため、記述したサポートコードに影響を与えることなく、状態マシンの構造に戻って操作することができます。
申し訳ありませんが、私は熱狂的すぎて、間違いなく全員を先延ばしにしています。しかし、これは一流のユーティリティであり、十分に文書化されています。
C / C ++ Users Journalの記事が優れていたMiro Samek(ブログState Space、ウェブサイトState Machines&Tools)の作業を必ずチェックしてください。
ウェブサイトには、ステートマシンフレームワーク(QPフレームワーク)、イベントハンドラー(QEP)、基本的なモデリングツール(QM)、トレースツール(QSpy)のオープンソースライセンスと商用ライセンスの両方で完全な(C / C ++)実装が含まれています。ステートマシンを描画し、コードを作成してデバッグすることができます。
この本には、実装の内容と理由、およびその使用方法に関する広範な説明が含まれており、階層的で有限な状態機械の基本を理解するための優れた資料でもあります。
Webサイトには、組み込みプラットフォームでソフトウェアを使用するためのいくつかのボードサポートパッケージへのリンクも含まれています。
私はpaxdiabloが説明するものと同様のことを行いましたが、状態/イベント遷移の配列の代わりに、イベント値を1つの軸のインデックスとして、現在の状態値を次のように、関数ポインターの2次元配列を設定しました。他の。それから私は電話をするstate = state_table[event][state](params)
と、正しいことが起こります。もちろん、無効な状態/イベントの組み合わせを表すセルは、そのように言う関数へのポインタを取得します。
明らかに、これが機能するのは、状態とイベントの値が両方とも隣接する範囲であり、0から始まるか、十分に近い場合だけです。
#define STATE_LIST() \STATE_LIST_ENTRY(state1)\STATE_LIST_ENTRY(state2)\...
(各の後に暗黙の改行\
)を定義します。例-状態名の配列の作成:#define STATE_LIST_ENTRY(s) #s , \n const char *state_names[] = { STATE_LIST() };\n #undef STATE_LIST_ENTRY
。最初に設定する作業もありますが、これは非常に強力です。新しい状態を追加->ミスがないことが保証されています。
非常に優れたテンプレートベースのC ++ステートマシン「フレームワーク」は、Stefan Heinzmannの記事で紹介されています。
記事には完全なコードダウンロードへのリンクがないため、自由にコードをプロジェクトに貼り付けてチェックアウトしました。以下のものはテストされ、いくつかのマイナーですが、明らかな欠けている部分が含まれています。
ここでの主な革新は、コンパイラーが非常に効率的なコードを生成していることです。空の入口/出口アクションには費用がかかりません。空でないエントリ/終了アクションはインライン化されます。コンパイラーは、ステートチャートの完全性も検証しています。アクションがないと、リンクエラーが発生します。捕まらない唯一のものは行方不明Top::init
です。
Samekのコードは設計により終了/遷移を処理しないのに対し、これは、Miro Samekの実装に代わる非常に優れた方法であり、何も欠落せずに対応できる場合、これはUMLセマンティクスを正しく実装しますが、完全なUMLステートチャート実装からはほど遠いです。 / 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)
ステートマシン(プログラム制御用の少なくとも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);
}
}
その後、個々の状態関数は入力をオンに切り替えて、適切な値を処理して返すことができます。
最も単純なケース
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行程度だと思います)、各状態ハンドラーを別々のファイルに入れます。各状態ハンドラーが2、3の画面より長くなると、状態スイッチが分割された方法と同様に、各イベントを個別の関数に分割します。これは、状態とは別に、または共通のテーブルを使用して、またはさまざまなスキームを組み合わせて、さまざまな方法で行うことができます。それらのいくつかはここで他によってカバーされました。速度が必要な場合は、テーブルを並べ替えてバイナリ検索を使用します。
一般的なプログラミング
テーブルの並べ替えや、説明からの状態マシンの生成などの問題にプリプロセッサが対処して、「プログラムに関するプログラムを作成」できるようにする必要があります。これはBoostの人々がC ++テンプレートを利用するためのものだと思いますが、構文は不可解です。
2次元テーブル
私は過去に状態/イベントテーブルを使用しましたが、最も単純なケースでは、それらを必要とはせず、1つの画面を超えても、switchステートメントの明快さと読みやすさを好むと言わなければなりません。より複雑なケースでは、他の人が指摘しているように、テーブルはすぐに手に負えなくなります。ここで紹介するイディオムを使用すると、メモリを消費するテーブルを(プログラムメモリであっても)維持する必要なく、必要に応じて多数のイベントと状態を追加できます。
免責事項
特別なニーズがあると、これらのイディオムはあまり役に立たなくなる可能性がありますが、非常に明確で保守しやすいことがわかりました。
非常にテストされていませんが、コードを書くのは楽しいですが、私の元の回答よりも洗練されたバージョンになりました。最新バージョンは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;
}
私は本当にpaxdiableの答えが好きで、ガード変数やステートマシン固有のデータなど、アプリケーションに不足している機能をすべて実装することにしました。
実装をこのサイトにアップロードして、コミュニティと共有しました。ARM用IAR Embedded Workbenchを使用してテストされています。
もう1つの興味深いオープンソースツールは、statecharts.orgのYakindu Statechart Toolsです。Harelステートチャートを利用して、階層的で並列的な状態を提供し、CおよびC ++(およびJava)コードを生成します。ライブラリは使用しませんが、「プレーンコード」アプローチに従います。コードは基本的にスイッチケース構造を適用します。コードジェネレーターもカスタマイズできます。さらに、ツールは他の多くの機能を提供します。
これは(いつものように)遅くなりますが、今日までの回答をスキャンすると、重要な何かが欠けていると思います。
私は自分のプロジェクトで、すべての有効な状態/イベントの組み合わせに対して関数を持たないことが非常に役立つ場合があることを発見しました。状態/イベントの2Dテーブルを効果的に作成するというアイデアが気に入っています。しかし、私はテーブルの要素が単純な関数ポインタ以上のものであることが好きです。代わりに私は自分のデザインを整理しようと心がけているので、それは心臓部にシンプルな原子要素やアクションの束を構成しています。そうすれば、状態/イベントテーブルの各交点にこれらの単純なアトミック要素をリストできます。考え方は、Nの二乗(通常は非常に単純な)関数の質量を定義する必要がないということです。エラーが発生しやすく、時間がかかり、書き込みや読み取りが困難なものに名前を付けるのはなぜですか?
また、オプションの新しい状態と、表の各セルのオプションの関数ポインターも含めます。関数ポインタは、アトミックアクションのリストを起動するだけでは不十分な例外的なケースのためにあります。
新しいコードを記述せずに、テーブルを編集するだけでさまざまな機能を表現できる場合は、正しく実行していることがわかります。
Alrght、私は私のものは他の誰のものとも少し違うと思います。他の答えで見るよりもコードとデータの分離が少し多い。私はこれを書くための理論を本当に読みました。これは完全な正規言語を実装します(悲しいことに正規表現なし)。ウルマン、ミンスキー、チョムスキー。私はそれをすべて理解したとは言えませんが、私は古いマスターからできるだけ直接引き付けました。彼らの言葉を通してです。
「はい」状態または「いいえ」状態への遷移を決定する述部への関数ポインターを使用します。これにより、よりアセンブリ言語のような方法でプログラムする通常の言語の有限状態アクセプターの作成が容易になります。私の愚かな名前の選択に気を取られないでください。'czek' == 'check'。'grok' == [ハッカー辞書で調べてください]。
したがって、反復ごとに、czekは現在の文字を引数として述語関数を呼び出します。述語がtrueを返す場合、文字が消費され(ポインタが進んで)、「y」遷移に従って次の状態を選択します。述語がfalseを返す場合、文字は消費されず、「n」遷移に従います。したがって、すべての命令は双方向分岐です!当時、メルの物語を読んでいたに違いありません。
このコードは私のポストスクリプトインタープリタから直接取得され、comp.lang.cのフェローからの多くのガイダンスを受けて現在の形式に進化しました。ポストスクリプトには基本的に構文がないので(大括弧のみが必要です)、このような通常の言語アクセプターはパーサーとしても機能します。
/* 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ステートチャートからコードへの直接遷移パスを提供します。
ここだ2間の比較を求めSOの質問著者の両方に対応しています。
やや複雑な制御ロジックに関するこの一連の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);
}
}
goto
それを使用すると、模倣的なマルチタスクOSへの依存関係が作成されます。
C ++を使用してOOコードを使用できることを示唆しているので、「GoF」状態パターンを評価することをお勧めします(GoF = Gang of Four、デザインパターンを脚光を浴びたデザインパターンの本を書いた人)。
特に複雑ではなく、広く使用および議論されているため、例や説明をオンラインで簡単に見ることができます。
また、後日あなたのコードを保守している他の人にも認識される可能性が非常に高くなります。
効率が心配な場合は、多くの要因がパフォーマンスに影響し、必ずしもOOが悪く機能的なコードが優れているとは限らないため、非OOアプローチがより効率的であることを確認することは、実際にベンチマークする価値があります。同様に、メモリ使用量が制約である場合も、状態パターンを使用する場合に特定のアプリケーションでこれが実際に問題になるかどうかを確認するために、いくつかのテストまたは計算を行う価値があります。
クレイグが示唆するように、以下は「Gof」状態パターンへのリンクです。
これは、メッセージキューをイベントとして使用する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
あなたの質問はかなり一般的です、
ここに役立つかもしれない2つの参考記事があります、
この記事では、組み込みシステム用のステートマシンを実装する簡単な方法について説明します。この記事では、ステートマシンは、少数の状態のいずれかになる可能性があるアルゴリズムとして定義されています。状態は、入力と出力、および入力と次の状態の所定の関係を引き起こす条件です。
この記事で説明するステートマシンはMealyマシンであることをよく知っている読者ならすぐにわかるでしょう。Mealyマシンは、出力が状態のみの関数であるMooreマシンとは対照的に、出力が現在の状態と入力の両方の関数である状態マシンです。
この記事の私の関心は、ステートマシンの基礎と、CまたはC ++でステートマシンをコーディングするための簡単なプログラミングガイドラインです。これらの単純な手法がより一般的になり、ソースコードからステートマシンの構造を簡単に確認できるようになることを願っています。
JavaおよびPythonプロジェクトでState Machine Compilerを使用して成功しました。
これは多くの回答が含まれている古い投稿ですが、Cの有限状態マシンに独自のアプローチを追加するつもりでした。Pythonスクリプトを作成して、任意の数の状態のスケルトンCコードを生成しました。そのスクリプトはFsmTemplateCのGituHubに文書化されています
この例は、私が読んだ他のアプローチに基づいています。gotoまたはswitchステートメントを使用しませんが、代わりにポインターマトリックス(ルックアップテーブル)に遷移関数があります。このコードは、大きな複数行の初期化マクロとC99の機能(指定された初期化子と複合リテラル)に依存しているため、これらが気に入らない場合は、このアプローチが気に入らない可能性があります。
FsmTemplateCを使用してスケルトンCコードを生成するターンスタイルの例の Pythonスクリプトを次に示します。
# 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);
}
ヘッダービジネスや、シンプルで高速なインターフェイスを実現するためのすべての機能は、私の心に留めておく価値があります。
オープンソースライブラリOpenFSTを使用できます。
OpenFstは、重み付き有限状態トランスデューサー(FST)を構築、結合、最適化、および検索するためのライブラリーです。重み付き有限状態トランスデューサは、各遷移に入力ラベル、出力ラベル、および重みがあるオートマトンです。より一般的な有限状態アクセプターは、各遷移の入力ラベルと出力ラベルが等しいトランスデューサーとして表されます。有限状態アクセプターは、ストリングのセット(特に、正規または有理セット)を表すために使用されます。有限状態トランスデューサーは、ストリングのペア間のバイナリ関係(具体的には、有理変換)を表すために使用されます。重みは、特定の遷移を行うためのコストを表すために使用できます。
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;
}
私は個人的に、自己参照構造体をポインター配列と組み合わせて使用しています。しばらく前にgithubにチュートリアルをアップロードしました。リンク:
https://github.com/mmelchger/polling_state_machine_c
注:このスレッドはかなり古くなっていますが、ステートマシンの設計についての意見や考えを知り、Cで可能なステートマシン設計の例を提供できるようにしたいと思っています。
UML-state-machine-in-cはCの「軽量」状態マシンフレームワークと考えることができます。このフレームワークは、有限状態マシンと階層型状態マシンの両方をサポートするように作成しました。ステートテーブルや単純なスイッチケースと比較して、フレームワークアプローチはよりスケーラブルです。単純な有限状態機械から複雑な階層状態機械まで使用できます。
状態機械はstate_machine_t
構造によって表されます。2つのメンバー「Event」と「state_t」へのポインターのみが含まれています。
struct state_machine_t
{
uint32_t Event; //!< Pending Event for state machine
const state_t* State; //!< State of state machine.
};
state_machine_t
ステートマシン構造の最初のメンバーである必要があります。例えば
struct user_state_machine
{
state_machine_t Machine; // Base state machine. Must be the first member of user derived state machine.
// User specific state machine members
uint32_t param1;
uint32_t param2;
...
};
state_t
状態のハンドラーと、開始および終了アクションのオプションのハンドラーが含まれます。
//! finite state structure
struct finite_state{
state_handler Handler; //!< State handler to handle event of the state
state_handler Entry; //!< Entry action for state
state_handler Exit; //!< Exit action for state.
};
フレームワークが階層状態マシン用に構成されている場合、 state_t
親と子の状態へのポインターが含まれます。
フレームワークは、dispatch_event
状態マシンにイベントをディスパッチし、switch_state
状態遷移をトリガーするAPI を提供します。
階層型ステートマシンの実装方法の詳細については、GitHubリポジトリを参照してください。
コード例、
https://github.com/kiishor/UML-State-Machine-in-C/blob/master/demo/simple_state_machine/readme.md https://github.com/kiishor/UML-State-Machine-in-C /blob/master/demo/simple_state_machine_enhanced/readme.md
以下は、各関数が独自の状態セットを持つことができるようにマクロを使用するステートマシンのメソッドです。https://www.codeproject.com/Articles/37037/Macros-to-simulate-multi-tasking-blocking-code -で
それは「マルチタスクのシミュレーション」と題されていますが、それが唯一の用途ではありません。
このメソッドは、コールバックを使用して、中断した各関数をピックアップします。各関数には、各関数に固有の状態のリストが含まれています。中央の「アイドルループ」は、ステートマシンを実行するために使用されます。「アイドルループ」は、ステートマシンがどのように機能するかを知りません。「何をすべきかを知っている」のは個々の機能です。関数のコードを記述するために、状態のリストを作成し、マクロを使用して「一時停止」および「再開」するだけです。Nexus 7000スイッチのトランシーバーライブラリを書いたとき、これらのマクロをシスコで使用しました。